openapi: 3.1.0
info:
  title: Xberg Enterprise API
  description: Document extraction API for Xberg Enterprise. Supports asynchronous extraction with webhook delivery. Audio/video transcription is currently available only through Xberg Core/CLI; this API rejects those inputs.
  contact:
    name: Xberg Enterprise
    url: https://example.com
    email: contact@xberg.io
  license:
    name: Proprietary
  version: 1.0.0
paths:
  /healthz:
    get:
      tags:
        - health
      summary: Check service liveness
      description: |-
        Always answers `200` once the process has started; it does not check any
        dependency (database, NATS) — use `GET /readyz` for that. Unauthenticated:
        intended for a Kubernetes liveness probe, not for API clients. The body
        also reports the deployment `tier` and whether auto-tune is enabled, which
        the frontend uses for feature gating.
      operationId: healthz
      responses:
        "200":
          description: Service is alive
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HealthResponse'
              example:
                status: ok
                tier: enterprise
                auto_tune:
                  enabled: true
                rag:
                  enabled: true
      security: []
  /readyz:
    get:
      tags:
        - health
      summary: Check service readiness
      description: |-
        Probes the primary database, the maintenance database, and NATS, and
        answers `200` with each dependency's status only when all three are
        reachable; any dependency failure answers `503` with the same body shape
        so the caller can see which check failed. Unauthenticated: intended for a
        Kubernetes readiness probe, not for API clients.
      operationId: readyz
      responses:
        "200":
          description: Service is ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReadinessResponse'
              example:
                status: ready
                checks:
                  database: ok
                  maintenance_database: ok
                  nats: ok
        "503":
          description: Service is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReadinessResponse'
      security: []
  /v1/audit:
    get:
      tags:
        - audit
      summary: List audit log entries for the caller's project
      description: |-
        Returns a page of `audit_log` rows for the credential's own project, most
        recent first, optionally filtered to entries whose `action` matches
        exactly (e.g. `"job.submit"`). Unlike Pro's `GET /v1/audit`, which is
        gated to an admin or the project's session-JWT owner, any of the
        project's API keys may read this endpoint — Enterprise has no per-key
        role concept, so every project key already has full read/write of the
        project's data plane.
      operationId: list_audit_entries
      parameters:
        - name: action
          in: query
          description: |-
            Filter to entries matching a known audit action exactly (e.g. `"job.submit"`).
            Unknown actions are rejected with a 400 response.
          required: false
          schema:
            type: string
            enum:
              - rag_collection.create
              - rag_collection.migrate
              - rag_document.delete
              - rag_collection.delete
              - project.create
              - project.delete
              - api_key.create
              - api_key.revoke
              - api_key.regenerate
              - integration.create
              - integration.delete
              - integration.disconnect
              - job.submit
              - crawl.submit
              - project.rag_config_update
              - auth.login
              - auth.failure
              - invitation.create
              - invitation.revoke
              - invitation.accept
              - member.role_update
              - member.remove
              - member.leave
              - project_role.create
              - project_role.update
              - project_role.delete
              - project_role.assign
              - webhook.create
              - webhook.delete
              - preset.create
              - preset.update
              - preset.delete
              - scim.connection.create
              - scim.connection.rotate
              - scim.group_roles.replace
              - db.api_keys.insert
              - db.api_keys.delete
              - db.api_keys.update
              - db.integrations.insert
              - db.integrations.delete
              - db.oauth_connections.update
              - db.audit_log.backfill
            maxLength: 64
        - name: limit
          in: query
          description: Page size. Values outside 1..=100 are rejected.
          required: false
          schema:
            type: integer
            format: int64
            default: 50
            maximum: 100
            minimum: 1
        - name: offset
          in: query
          description: Number of items to skip. Values outside 0..=1,000,000 are rejected.
          required: false
          schema:
            type: integer
            format: int64
            default: 0
            maximum: 1000000
            minimum: 0
      responses:
        "200":
          description: Audit entries listed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListAuditEntriesResponse'
        "400":
          description: Invalid action or pagination parameters
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: An API key's project_id or x-project-id names a project other than the key's own
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: The current session has no membership in the requested project
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "503":
          description: Session authentication is temporarily unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/auto-tune:
    get:
      tags:
        - auto-tune
      summary: List auto-tune jobs
      description: |-
        Returns a page of job summaries (status and timestamps, not the ground
        truth or the winning configuration), most recently submitted first. Fetch
        `GET /v1/auto-tune/{id}` for one job's live status or
        `GET /v1/auto-tune/{id}/result` for a completed job's result.
      operationId: list_auto_tune_jobs
      parameters:
        - name: limit
          in: query
          description: Page size (default 50, accepted range 1..=100).
          required: false
          schema:
            type: integer
            format: int64
            default: 50
            maximum: 100
            minimum: 1
        - name: offset
          in: query
          description: Pagination offset (default 0, accepted range 0..=1,000,000).
          required: false
          schema:
            type: integer
            format: int64
            default: 0
            maximum: 1000000
            minimum: 0
      responses:
        "200":
          description: Auto-tune jobs for the current project
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListAutoTuneJobsResponse'
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Missing or invalid authentication
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: An API key's project_id or x-project-id names a project other than the key's own
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Auto-tune is disabled or the project is unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "422":
          description: Request fails validation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "500":
          description: Persistence error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "503":
          description: Session authentication is temporarily unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
    post:
      tags:
        - auto-tune
      summary: Submit a new auto-tune job
      description: |-
        Accepts `multipart/form-data`: a `request` text part carrying a
        JSON-encoded `CreateAutoTuneJobRequest`, plus one binary part per
        document referenced by `documents[].filename`.
      operationId: submit_auto_tune
      requestBody:
        content:
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/AutoTuneMultipartForm'
        required: true
      responses:
        "201":
          description: Auto-tune job created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateAutoTuneJobResponse'
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Missing or invalid authentication
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: The API key is read-only and cannot use this method
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: The current session has no membership in the requested project
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "413":
          description: The request body is larger than this deployment accepts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "415":
          description: 'The request body was not sent with Content-Type: application/json'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "422":
          description: Request fails validation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Auto-tune admission limit exceeded
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "500":
          description: Persistence or dispatch error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "503":
          description: Session authentication is temporarily unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/auto-tune/capabilities:
    get:
      tags:
        - auto-tune
      summary: Get auto-tune capabilities (OCR backends and tunable knobs) for this deployment
      description: |-
        Requires the same auth as its sibling endpoints even though the response itself is
        project-agnostic, so it sits behind the same access boundary as the rest of the surface.
      operationId: get_auto_tune_capabilities
      responses:
        "200":
          description: Auto-tune capabilities
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AutoTuneCapabilitiesResponse'
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Missing or invalid authentication
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: An API key's project_id or x-project-id names a project other than the key's own
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: The current session has no membership in the requested project
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "422":
          description: Request fails validation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "503":
          description: Session authentication is temporarily unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/auto-tune/{id}:
    get:
      tags:
        - auto-tune
      summary: Get auto-tune job status
      description: |-
        Auto-tune runs a search over extraction configurations against a
        caller-supplied ground truth, scoring each candidate and tracking the
        best one seen so far. Returns the job's current lifecycle state and
        progress; once the state reaches a terminal value, fetch
        `GET /v1/auto-tune/{id}/result` for the winning configuration and its
        score.
      operationId: get_auto_tune_status
      parameters:
        - name: id
          in: path
          description: Auto-tune job ID
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Auto-tune job status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AutoTuneJobStatus'
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Missing or invalid authentication
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: An API key's project_id or x-project-id names a project other than the key's own
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Job not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "422":
          description: Request fails validation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "500":
          description: Persistence error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "503":
          description: Session authentication is temporarily unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
    delete:
      tags:
        - auto-tune
      summary: Delete an auto-tune job
      description: |-
        Irreversible: there is no soft-delete. Rejected with `409` while the job
        is still running: stop it first with `POST /v1/auto-tune/{id}/stop`, which
        drives it to a terminal state, or wait for it to finish.
        Rejected with `403` for a read-only API key.
      operationId: delete_auto_tune_job
      parameters:
        - name: id
          in: path
          description: Auto-tune job ID
          required: true
          schema:
            type: string
      responses:
        "204":
          description: Auto-tune job deleted
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Missing or invalid authentication
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: The API key is read-only and cannot use this method
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Job not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "409":
          description: Job is still running and cannot be deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "422":
          description: Request fails validation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "500":
          description: Persistence error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "503":
          description: Session authentication is temporarily unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/auto-tune/{id}/promote:
    post:
      tags:
        - auto-tune
      summary: Promote an auto-tune result to a tuning profile
      description: |-
        Persists the job's best-found configuration under the name in
        `PromoteProfileRequest`, so it can be applied to future extractions
        without re-running the search. Rejected with `409` when the job has not
        reached a successful terminal state, when no trial in the run scored any
        document (`allow_unscored` promotes such a run anyway), or when the project
        already has a tuning profile with this name; rejected with `403` for a
        read-only API key.
      operationId: promote_auto_tune_profile
      parameters:
        - name: id
          in: path
          description: Auto-tune job ID
          required: true
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PromoteProfileRequest'
        required: true
      responses:
        "201":
          description: Tuning profile created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TuningProfileDetail'
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Missing or invalid authentication
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: The API key is read-only and cannot use this method
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Job not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "409":
          description: The job has not completed, has failed, scored no document, or the name is already taken
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "413":
          description: The request body is larger than this deployment accepts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "415":
          description: 'The request body was not sent with Content-Type: application/json'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "422":
          description: Request fails validation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "500":
          description: Persistence error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "503":
          description: Session authentication is temporarily unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/auto-tune/{id}/result:
    get:
      tags:
        - auto-tune
      summary: Get auto-tune job result
      description: |-
        Returns the best extraction configuration the search found, its score
        against the supplied ground truth, and the provenance of how it was
        reached. Rejected with `409` when the job has not yet reached a
        successful terminal state — including when it failed, since there is no
        winning configuration to return in that case.
      operationId: get_auto_tune_result
      parameters:
        - name: id
          in: path
          description: Auto-tune job ID
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Auto-tune job result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AutoTuneResult'
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Missing or invalid authentication
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: An API key's project_id or x-project-id names a project other than the key's own
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Job not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "409":
          description: Job exists but has not completed (or has failed)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "422":
          description: Request fails validation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "500":
          description: Persistence error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "503":
          description: Session authentication is temporarily unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/auto-tune/{id}/stop:
    post:
      tags:
        - auto-tune
      summary: Stop a running auto-tune job
      description: |-
        The stop is cooperative: work already in flight may finish before the run
        observes the request at a progress boundary and completes with its partial
        leaderboard. `GET /v1/auto-tune/{id}/result` still serves the best
        configuration found. A terminal job answers `204` unchanged, so callers
        may safely retry.
      operationId: stop_auto_tune_job
      parameters:
        - name: id
          in: path
          description: Auto-tune job ID
          required: true
          schema:
            type: string
      responses:
        "204":
          description: Stop requested, or the job had already finished
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Missing or invalid authentication
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: The API key is read-only and cannot use this method
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Job not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "409":
          description: The job cannot be stopped
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "422":
          description: Request fails validation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "500":
          description: Persistence error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "503":
          description: Session authentication is temporarily unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/crawl-jobs/{crawl_job_id}/events:
    get:
      tags:
        - jobs
      summary: Stream crawl job events
      description: |-
        Authenticates the request, verifies project ownership of the crawl job,
        opens an ephemeral NATS subscriber for active crawls, and streams events with a 15s heartbeat.
        Durable state replays a terminal event without NATS for already-finished jobs,
        or reconciles a crawl-phase Complete with the later job-terminal row.

        Each data frame is a default `message` event carrying JSON. The `kind` discriminator is derived from the NATS subject
        before serialization. A 15-second SSE comment heartbeat keeps the stream alive.
        <!-- public SSE wire contract -->

        # Response format
        - **200 OK** with `text/event-stream` media type and events in JSON with `kind` field
        - **401 Unauthorized** if authentication fails
        - **403 Forbidden** if the crawl job belongs to a different project
        - **404 Not Found** if the crawl job does not exist
        - **429 Too Many Requests** if the project already holds its maximum number of streams
        - **500 Internal Server Error** if the crawl job lookup fails
        - **503 Service Unavailable** if NATS subscription fails
      operationId: stream_crawl_events
      parameters:
        - name: crawl_job_id
          in: path
          description: Crawl job ID (UUID)
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Crawl events SSE stream
          content:
            text/event-stream:
              schema:
                $ref: '#/components/schemas/CrawlEventV1'
        "400":
          description: 'The request is malformed: a path parameter, a query parameter or the body did not parse'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: Crawl job belongs to different project
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Crawl job not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Too many concurrent event streams for this project
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "500":
          description: Failed to fetch the crawl job
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "503":
          description: NATS subscription failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/documents/{document_id}:
    get:
      tags:
        - documents
      summary: Get the latest version of a document with its extraction result.
      description: |-
        A "document" here is the lineage a caller-supplied `document_id` ties
        together across repeated extractions of the same logical file (see
        `POST /v1/extract`'s multipart `document_id-<filename>` part). Returns the
        most recently created version's metadata plus its `ExtractionResult` when
        one is available; `result` is `null`, not omitted, when the latest version
        has no completed extraction yet or its stored result could not be read
        back.
      operationId: get_latest
      parameters:
        - name: document_id
          in: path
          description: Logical document ID
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Latest version with extraction result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LatestDocumentResponse'
        "400":
          description: Invalid document ID
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: An API key's project_id or x-project-id names a project other than the key's own
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Document not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "503":
          description: Session authentication is temporarily unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/documents/{document_id}/diff:
    get:
      tags:
        - documents
      summary: Compute the diff between two versions of a document.
      description: |-
        `from` and `to` each name a version by its sequence number or by the job
        ID that produced it; naming the same version for both is a `400`. A diff
        already computed for the same pair is served from its cached result. A
        new diff is attempted inline with a 2-second budget: if it finishes in
        time the response is `200` with the `DiffResponse` body; if not, the
        computation continues in the background and the response is `202` with a
        `diff_job_id` to poll via `GET /v1/documents/{document_id}/diff/{diff_job_id}`.
      operationId: get_diff
      parameters:
        - name: document_id
          in: path
          description: Logical document ID
          required: true
          schema:
            type: string
            format: uuid
        - name: from
          in: query
          description: Source version (sequence integer or job UUID)
          required: true
          schema:
            oneOf:
              - type: integer
                format: int32
                maximum: 2147483647
                minimum: -2147483648
              - type: string
                format: uuid
        - name: to
          in: query
          description: Target version (sequence integer or job UUID)
          required: true
          schema:
            oneOf:
              - type: integer
                format: int32
                maximum: 2147483647
                minimum: -2147483648
              - type: string
                format: uuid
      responses:
        "200":
          description: Diff computed inline
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DiffResponse'
        "202":
          description: Diff queued for async computation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DiffAsyncAccepted'
        "400":
          description: Invalid parameters
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: An API key's project_id or x-project-id names a project other than the key's own
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Version not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "409":
          description: Version extraction is unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "500":
          description: Diff computation failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "503":
          description: Session authentication is temporarily unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/documents/{document_id}/diff/{diff_job_id}:
    get:
      tags:
        - documents
      summary: Poll the status of an asynchronously-computed diff.
      description: |-
        Reads back a `diff_job_id` returned by the `202` fallback from
        `GET /v1/documents/{document_id}/diff`. Answers `202` with a
        `DiffAsyncAccepted` body while the computation is still pending, and `200`
        with the `DiffResponse` body once it completes. A diff job that itself
        cannot read a required extraction surfaces as a `409` with an actionable
        reason; storage transport failures remain pending for retry.
      operationId: poll_diff
      parameters:
        - name: document_id
          in: path
          description: Logical document ID
          required: true
          schema:
            type: string
            format: uuid
        - name: diff_job_id
          in: path
          description: Async diff job ID returned by the 202 fallback
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Diff result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DiffResponse'
        "202":
          description: Diff still pending
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DiffAsyncAccepted'
        "400":
          description: Invalid document ID or diff job ID
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: An API key's project_id or x-project-id names a project other than the key's own
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Diff job not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "409":
          description: Version extraction is unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "500":
          description: Diff computation failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "503":
          description: Session authentication is temporarily unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/documents/{document_id}/versions:
    get:
      tags:
        - documents
      summary: List all versions of a document.
      description: |-
        Returns every extraction that shares the given `document_id`, newest
        first, each as a `DocumentVersionEntry` carrying that version's job
        metadata (status, filename, page/byte counts, timestamps) but not its
        extraction result — fetch `GET /v1/documents/{document_id}` for the latest
        version's result, or resolve a specific version's job ID from this list
        and use it as the `from`/`to` parameter of the diff endpoint.
      operationId: list_versions
      parameters:
        - name: document_id
          in: path
          description: Logical document ID
          required: true
          schema:
            type: string
            format: uuid
        - name: limit
          in: query
          description: Maximum versions to return. A negative value is rejected with a 422; a value above 200 is accepted but clamped to 200.
          required: false
          schema:
            type: integer
            format: int32
            minimum: 0
        - name: offset
          in: query
          description: Versions to skip. A negative value is clamped to 0; a value above 1,000,000 is clamped to 1,000,000.
          required: false
          schema:
            type: integer
            format: int32
      responses:
        "200":
          description: Document versions
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/DocumentVersionEntry'
        "400":
          description: 'The request is malformed: a path parameter, a query parameter or the body did not parse'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: An API key's project_id or x-project-id names a project other than the key's own
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: The current session has no membership in the requested project
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "422":
          description: Invalid document ID or pagination parameters
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "503":
          description: Session authentication is temporarily unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/enrich:
    post:
      tags:
        - enrich
      summary: Submit text for enrichment (async).
      description: |-
        Accepts text with optional enrichment flags and returns a job ID for polling.
        Validates text size, publishes a message to NATS, and writes initial status to KV.

        # Returns
        - 202 Accepted with `{ job_id }`
        - 400 Bad Request if text exceeds size limit or options invalid
        - 500 Internal Server Error if NATS/KV write fails
      operationId: submit_enrich
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EnrichTextRequest'
        required: true
      responses:
        "202":
          description: Enrichment job accepted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EnrichJobSubmitted'
        "400":
          description: Text or labels exceed their limits, or the request asks for classification, chunk_classification, or captioning, none of which this tier implements (Enterprise's async worker runs only keywords, entities, and labels; those three run synchronously on Pro)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: An API key's project_id or x-project-id names a project other than the key's own
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: The current session has no membership in the requested project
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "413":
          description: The request body is larger than this deployment accepts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "415":
          description: 'The request body was not sent with Content-Type: application/json'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "500":
          description: Job status store or queue unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "503":
          description: Session authentication is temporarily unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/enrich/{job_id}:
    get:
      tags:
        - enrich
      summary: Poll enrichment job status.
      description: |-
        Retrieves job status from the KV bucket. Status transitions:
        - Pending: job is queued or processing
        - Completed: includes enrichment result
        - Failed: includes error message

        # Returns
        - 200 OK with status and result
        - 404 Not Found if job ID is unknown
        - 500 Internal Server Error if KV read fails
      operationId: get_enrich_status
      parameters:
        - name: job_id
          in: path
          description: Enrichment job ID (ULID)
          required: true
          schema:
            type: string
            maxLength: 26
            minLength: 26
            pattern: ^[0-9A-HJKMNP-TV-Za-hjkmnp-tv-z]{26}$
      responses:
        "200":
          description: Job status, with the result when completed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EnrichJobStatus'
        "400":
          description: Job ID is not a ULID
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: An API key's project_id or x-project-id names a project other than the key's own
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Job not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "500":
          description: Job status store unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "503":
          description: Session authentication is temporarily unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/extract:
    post:
      tags:
        - extract
      summary: '`POST /v1/extract` — Submit documents for extraction'
      description: |-
        Accepts `application/json` or `multipart/form-data`.

        **JSON body**: `{"documents": [...], "options": {...}, "webhook": {"url": "...", "secret": "...", "metadata": {...}}}`

        **Multipart**: file parts (binary) + `webhook` part (JSON string) + optional `options` part (JSON string)

        A request accepts at most 10 documents because this path can decode inline
        bytes and retains the full batch while creating jobs. The cap bounds request
        memory and immediate job dispatch. Use presigned uploads to stage up to 100
        document metadata entries and upload their bytes directly to storage.
        <!-- public batch-limit contract -->

        Returns 202 Accepted with job IDs. Results are delivered via the configured webhook.

        Sessions may select a project through `X-Project-ID` after current membership is verified.
        API keys remain scoped to their own project. A body `project_id` must match the resolved project.
      operationId: extract
      parameters:
        - name: X-Project-ID
          in: header
          description: Selected project; sessions require current membership, API keys remain scoped to their own project
          required: false
          schema:
            type: string
            format: uuid
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ExtractRequest'
            examples:
              File extraction:
                summary: Extract from base64-encoded documents
                value:
                  documents:
                    - filename: invoice.pdf
                      mime_type: application/pdf
                      data: SGVsbG8gV29ybGQ=
                  options:
                    extraction_config:
                      output_format: markdown
                      ocr:
                        backend: tesseract
                        language: eng
                  webhook:
                    url: https://example.com/webhook
                    secret: my-secret
                    metadata:
                      request_id: abc123
              URL crawl extraction:
                summary: Crawl URLs and extract all discovered documents
                value:
                  urls:
                    - url: https://example.com/docs
                  crawl_config:
                    max_depth: 2
                    max_pages: 50
                    stay_on_domain: true
                  webhook:
                    url: https://example.com/webhook
                    secret: my-secret
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/ExtractMultipartForm'
        required: true
      responses:
        "202":
          description: Jobs accepted for processing
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExtractResponse'
              example:
                job_ids:
                  - 550e8400-e29b-41d4-a716-446655440000
                crawl_job_ids:
                  - 660e9400-f39c-51e5-b827-557766551111
                status: pending
        "400":
          description: Invalid request, an unavailable crawl network route, or a `project_id` that disagrees with the authorized selected project
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: validation
                  message: At least one document or URL is required
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: unauthorized
                  message: Missing project context
        "403":
          description: The API key is read-only and cannot use this method
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: An integration named in `documents` does not exist in the project
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "413":
          description: A decoded document is larger than the project's `max_file_size_mb`, or the request body is larger than `MAX_BODY_SIZE`
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "415":
          description: Content-Type is neither application/json nor multipart/form-data
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit or quota exceeded. The error code is `rate_limited` or `quota_exceeded`. Retry-After gives seconds until retry is safe for rate limits and monthly quotas; non-windowed quotas omit it.
          headers:
            Retry-After:
              schema:
                type: integer
                format: int64
                minimum: 0
              description: Seconds to wait; monthly quotas reset at the next UTC calendar month.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "503":
          description: Session authentication is temporarily unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/extractions:
    get:
      tags:
        - extractions
      summary: List structured-extraction audit events for a project.
      description: |-
        Each event is one preset-driven extraction call: the preset and version
        used, the call mode (`text_only`, `vision_only`, or `text_plus_vision`),
        page and vision-call counts, and token usage when the call succeeded.
        `days` bounds the lookback window from now; `total` in the response is
        the count of matching events in that window, independent of `limit`.
      operationId: list_extraction_events
      parameters:
        - name: days
          in: query
          description: Days to look back. Values outside 1..=365 are clamped into range rather than rejected.
          required: false
          schema:
            type: integer
            format: int64
        - name: limit
          in: query
          description: Page size. Values outside 1..=100 are clamped into range rather than rejected.
          required: false
          schema:
            type: integer
            format: int64
        - name: offset
          in: query
          description: Number of items to skip. A value above 1,000,000 is clamped to 1,000,000; a negative value is clamped to 0.
          required: false
          schema:
            type: integer
            format: int64
      responses:
        "200":
          description: Extraction events for the project
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListExtractionEventsResponse'
        "400":
          description: 'The request is malformed: a path parameter, a query parameter or the body did not parse'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: An API key's project_id or x-project-id names a project other than the key's own
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: The current session has no membership in the requested project
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "500":
          description: Persistence error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "503":
          description: Session authentication is temporarily unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/jobs:
    get:
      tags:
        - jobs
      summary: List jobs for the caller's project
      description: |-
        Returns a page of the project's extraction and crawl jobs, most recently
        created first, together with the `total` count of jobs in the project so
        a client can compute how many pages remain. Each entry is a `JobSummary`,
        not the full result — fetch `GET /v1/jobs/{id}/result` for a completed
        job's content. `project_id` selects a current session membership or an API key's own
        project; naming a different project is rejected rather than silently
        ignored.
      operationId: list_jobs
      parameters:
        - name: limit
          in: query
          description: Page size (default 50, max 100).
          required: false
          schema:
            type: integer
            format: int64
            default: 50
            maximum: 100
            minimum: 1
        - name: offset
          in: query
          description: Pagination offset (default 0, clamped to a maximum of 1,000,000).
          required: false
          schema:
            type: integer
            format: int64
            default: 0
            minimum: 0
        - name: project_id
          in: query
          description: |-
            Project to read. Sessions require current membership; API keys cannot select another project. A conflicting
            project is refused rather than silently ignored.
          required: false
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Jobs listed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListJobsResponse'
        "400":
          description: Invalid pagination parameters
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: An API key's project_id or x-project-id names a project other than the key's own
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: The current session has no membership in the requested project
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "503":
          description: Session authentication is temporarily unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/jobs/{id}:
    get:
      tags:
        - jobs
      summary: Get job status and results
      description: |-
        Accepts both extraction job IDs (returned in `job_ids`) and crawl job IDs
        (returned in `crawl_job_ids`) from `POST /v1/extract`. When the ID belongs
        to a crawl job, the response body uses the `CrawlJobResponse` variant of
        the canonical `GetJobResponse` schema.
      operationId: get_job
      parameters:
        - name: id
          in: path
          description: Job ID or crawl job ID (UUID)
          required: true
          schema:
            type: string
            format: uuid
          example: 550e8400-e29b-41d4-a716-446655440000
        - name: project_id
          in: query
          description: Project to read; sessions require current membership, API keys remain scoped to their own project
          required: false
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Job retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GetJobResponse'
              example:
                id: 550e8400-e29b-41d4-a716-446655440000
                job_type: extraction
                filename: invoice.pdf
                mime_type: application/pdf
                status: completed
                created_at: "2025-12-21T10:00:00Z"
                result:
                  content: 'Invoice total: $1,234.56'
                  mime_type: text/markdown
                  tables: []
                  images: []
                  metadata:
                    title: 'Invoice #12345'
        "400":
          description: Invalid job ID format
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: validation
                  message: Invalid job ID format
        "401":
          description: Unauthorized - missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: unauthorized
                  message: Missing or invalid API key
        "403":
          description: An API key's project_id or x-project-id names a project other than the key's own
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Job not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: job_not_found
                  message: Job not found
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "500":
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: internal
                  message: Database connection failed
        "503":
          description: Session authentication is temporarily unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
    delete:
      tags:
        - jobs
      summary: Cancel an extraction job
      description: |-
        Requests cancellation of a job in the caller's project. This is
        best-effort: a job already picked up by a worker may finish before the
        cancellation is observed, and a job that has already reached a terminal
        state (completed, failed, or already cancelled) answers `204` without
        changing anything. Returns `404` only when the job ID does not resolve to
        a job the caller's project owns. There is no response body on success —
        poll `GET /v1/jobs/{id}` to see the job's final status.
      operationId: cancel_job
      parameters:
        - name: id
          in: path
          description: Extraction job ID (UUID)
          required: true
          schema:
            type: string
            format: uuid
          example: 550e8400-e29b-41d4-a716-446655440000
      responses:
        "204":
          description: Cancellation accepted or job already terminal
        "400":
          description: Invalid job ID format
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: An API key's project_id or x-project-id names a project other than the key's own
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Job not found in the caller's project
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "500":
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "503":
          description: Service unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/jobs/{id}/result:
    get:
      tags:
        - jobs
      summary: Get job result
      description: 'The result envelope of a job in a terminal successful state (`completed` or `partial_success`). Xberg Pro serves the same envelope on the same route, so one polling loop works on both tiers: poll until the route stops answering 409. A result that has passed its retention period answers 404.'
      operationId: get_job_result
      parameters:
        - name: id
          in: path
          description: Job ID (UUID)
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Extraction result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JobResult'
        "400":
          description: 'The request is malformed: a path parameter, a query parameter or the body did not parse'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: An API key's project_id or x-project-id names a project other than the key's own
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Job not found, or its result has expired
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "409":
          description: Job has not reached a terminal successful state
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "503":
          description: Session authentication is temporarily unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/jobs/{job_id}/pages/{page_number}:
    get:
      tags:
        - jobs
      summary: Get page raster from extraction job
      description: |-
        Serves page images that were persisted during structured extraction.
        Page numbers are 1-indexed. Images are cached for 1 hour.

        # Response format
        - **200 OK** with PNG image data and cache headers
        - **404 Not Found** if the job does not exist or page is out of range
        - **401 Unauthorized** if authentication is required and not provided (via middleware)
        - **500 Internal Server Error** if storage access fails
      operationId: get_job_page
      parameters:
        - name: job_id
          in: path
          description: Extraction job ID (UUID)
          required: true
          schema:
            type: string
            format: uuid
        - name: page_number
          in: path
          description: 1-indexed page number
          required: true
          schema:
            type: integer
            format: int32
            minimum: 0
      responses:
        "200":
          description: Page PNG image
          content:
            image/png:
              schema:
                type: string
                format: binary
        "400":
          description: 'The request is malformed: a path parameter, a query parameter or the body did not parse'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: An API key's project_id or x-project-id names a project other than the key's own
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Job or page not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "500":
          description: Storage error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "503":
          description: Session authentication is temporarily unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/presets:
    get:
      tags:
        - presets
      summary: List the curated preset registry.
      description: |-
        Returns every built-in preset's summary (id, name, description) in one
        unpaginated response — the registry is small and fixed at build time, not
        a per-project resource. Public: no authentication required, since the
        preset library itself is not tenant-specific.
      operationId: list_presets
      responses:
        "200":
          description: Curated preset library
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/PresetSummary'
      security: []
  /v1/presets/{id}:
    get:
      tags:
        - presets
      summary: Get the full detail of one curated preset.
      description: |-
        Returns the preset's system prompt, output schema, and content
        fingerprint (a hash callers can use to detect that a preset's definition
        changed between releases). `404` when `id` does not match a preset in the
        registry. Public: no authentication required.
      operationId: get_preset
      parameters:
        - name: id
          in: path
          description: Preset identifier
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Preset detail
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PresetDetail'
        "400":
          description: 'The request is malformed: a path parameter, a query parameter or the body did not parse'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Preset not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security: []
  /v1/presets/{id}/sample/{name}:
    get:
      tags:
        - presets
      summary: Get a bundled sample file for a preset's landing-page preview.
      description: |-
        Streams the raw sample bytes with a `Content-Type` guessed from `name`'s
        extension; there is no wrapping envelope. `404` when either the preset or
        the named sample does not exist. Public: no authentication required.
      operationId: get_preset_sample
      parameters:
        - name: id
          in: path
          description: Preset identifier
          required: true
          schema:
            type: string
        - name: name
          in: path
          description: Sample file name
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Sample bytes; Content-Type follows the sample file
          content:
            '*/*':
              schema:
                type: string
                format: binary
        "400":
          description: 'The request is malformed: a path parameter, a query parameter or the body did not parse'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Preset or sample not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security: []
  /v1/rag/collections:
    get:
      tags:
        - rag
      summary: List RAG collections
      description: Returns paginated list of collections ordered by creation time (newest first).
      operationId: list_collections
      parameters:
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            format: int64
            default: 50
            maximum: 100
            minimum: 1
        - name: offset
          in: query
          required: false
          schema:
            type: integer
            format: int64
            default: 0
            maximum: 1000000
            minimum: 0
      responses:
        "200":
          description: List of collections
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListCollectionsResponse'
        "400":
          description: Invalid request parameters
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Missing or invalid bearer token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: An API key's project_id or x-project-id names a project other than the key's own
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: RAG is not enabled for this project
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "422":
          description: Request fails validation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "500":
          description: Database error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "503":
          description: Session authentication is temporarily unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
    post:
      tags:
        - rag
      summary: Create a new RAG collection
      description: |-
        Creates a vector collection with the specified embedding and index configuration.
        Collection names must be unique per project.
        Returns 201 on success, 409 if a collection with that name already exists.
      operationId: create_collection
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateCollectionRequest'
        required: true
      responses:
        "201":
          description: Collection created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateCollectionResponse'
        "400":
          description: 'Invalid request parameters, or a label classification field was supplied: `rag.label_classification.unsupported`'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Missing or invalid bearer token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: The API key is read-only and cannot use this method
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: RAG is not enabled for this project
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "409":
          description: Collection name already exists in this project
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "413":
          description: The request body is larger than this deployment accepts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "415":
          description: 'The request body was not sent with Content-Type: application/json'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "422":
          description: Request fails validation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "500":
          description: Database error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "503":
          description: Session authentication is temporarily unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/rag/collections/{name}:
    get:
      tags:
        - rag
      summary: Get a RAG collection
      description: Returns 404 if the collection does not exist or belongs to another project.
      operationId: get_collection
      parameters:
        - name: name
          in: path
          description: Collection name
          required: true
          schema:
            type: string
          example: my-docs
      responses:
        "200":
          description: Collection details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CollectionResponse'
        "400":
          description: Invalid request parameters
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Missing or invalid bearer token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: An API key's project_id or x-project-id names a project other than the key's own
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Collection not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "422":
          description: Request fails validation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "500":
          description: Database error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "503":
          description: Session authentication is temporarily unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
    delete:
      tags:
        - rag
      summary: Delete a RAG collection
      description: |-
        Marking the collection as deleted prevents new document uploads but
        does not immediately remove data.
        Returns 204 on success, 404 if the collection does not exist.
      operationId: delete_collection
      parameters:
        - name: name
          in: path
          description: Collection name
          required: true
          schema:
            type: string
          example: my-docs
      responses:
        "204":
          description: Collection deleted
        "400":
          description: Invalid request parameters
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Missing or invalid bearer token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: The API key is read-only and cannot use this method
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Collection not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "422":
          description: Request fails validation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "500":
          description: Database error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "503":
          description: Session authentication is temporarily unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/rag/collections/{name}/documents:
    post:
      tags:
        - rag
      summary: Create a document in a RAG collection
      description: |-
        Supports two ingest paths:
        - **Text source** → server chunks, enriches, embeds → 202 async
        - **Chunks source with every embedding supplied** → 200 synchronous
        - **Chunks source with any embedding missing** → worker handles embedding and enrichment → 202 async

        Quota checks apply: if the collection has `quota_max_documents` set and
        the active document count would exceed it, returns 429.
      operationId: create_document
      parameters:
        - name: name
          in: path
          description: Collection name
          required: true
          schema:
            type: string
          example: my-docs
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateDocumentRequest'
        required: true
      responses:
        "200":
          description: Document ingested synchronously with caller-supplied embeddings
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateDocumentResponse'
              example:
                document_id: 550e8400-e29b-41d4-a716-446655440000
                external_id: kb-article-42
                collection: my-docs
                chunk_count: 2
                embedding_dim: 768
                ingested_at: "2026-09-22T12:00:00Z"
        "202":
          description: Document ingest queued asynchronously
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateDocumentAsyncResponse'
              example:
                job_id: 01ARZ3NDEKTSV4RRFFQ69G5FAV
                document_id: 550e8400-e29b-41d4-a716-446655440000
                status: pending
                poll: /v1/rag/jobs/01ARZ3NDEKTSV4RRFFQ69G5FAV
        "400":
          description: Malformed request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Missing or invalid bearer token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: The API key is read-only and cannot use this method
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Collection not found or RAG disabled on project
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "413":
          description: The request body is larger than this deployment accepts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "415":
          description: 'The request body was not sent with Content-Type: application/json'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "422":
          description: Request fails validation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Collection quota exceeded
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "500":
          description: Persistence or dispatch error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "503":
          description: Session authentication is temporarily unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
    delete:
      tags:
        - rag
      summary: Delete documents from a RAG collection
      description: Accepts either a list of document IDs or a metadata filter expression.
      operationId: delete_documents
      parameters:
        - name: name
          in: path
          description: Collection name
          required: true
          schema:
            type: string
          example: my-docs
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DeleteDocumentsRequest'
        required: true
      responses:
        "200":
          description: Documents deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeleteDocumentsResponse'
              example:
                deleted_count: 5
        "400":
          description: Malformed request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Missing or invalid bearer token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: The API key is read-only and cannot use this method
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Collection not found or RAG disabled
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "413":
          description: The request body is larger than this deployment accepts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "415":
          description: 'The request body was not sent with Content-Type: application/json'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "422":
          description: Request fails validation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "500":
          description: Database error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "503":
          description: Session authentication is temporarily unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/rag/collections/{name}/documents/{id}/reindex:
    post:
      tags:
        - rag
      summary: Reindex a document
      description: |-
        When `re_extract: true`, the worker re-pulls the source object from storage
        and re-runs xberg extraction before re-chunking.
      operationId: reindex_document
      parameters:
        - name: name
          in: path
          description: Collection name
          required: true
          schema:
            type: string
          example: my-docs
        - name: id
          in: path
          description: Document ID
          required: true
          schema:
            type: string
            format: uuid
          example: 550e8400-e29b-41d4-a716-446655440000
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReindexRequest'
        required: true
      responses:
        "202":
          description: Reindex job queued
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateDocumentAsyncResponse'
              example:
                job_id: 01ARZ3NDEKTSV4RRFFQ69G5FAV
                document_id: 550e8400-e29b-41d4-a716-446655440000
                status: pending
                poll: /v1/rag/jobs/01ARZ3NDEKTSV4RRFFQ69G5FAV
        "400":
          description: Malformed request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Missing or invalid bearer token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: The API key is read-only and cannot use this method
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Document or collection not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "413":
          description: The request body is larger than this deployment accepts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "415":
          description: 'The request body was not sent with Content-Type: application/json'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "422":
          description: Request fails validation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "500":
          description: Persistence or dispatch error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "503":
          description: Session authentication is temporarily unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/rag/collections/{name}/migrate-embeddings:
    post:
      tags:
        - rag
      summary: Initiate embedding source migration
      description: |-
        Validates that the target version is greater than the current version
        and that the target embedding source has the same dimension as the
        current embedding source (cross-dimension migration is not supported).

        Returns 202 with a job_id for polling progress.
      operationId: migrate_embeddings
      parameters:
        - name: name
          in: path
          description: Collection name
          required: true
          schema:
            type: string
          example: my-docs
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MigrateEmbeddingsRequest'
        required: true
      responses:
        "202":
          description: Migration queued
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MigrateEmbeddingsResponse'
        "400":
          description: Malformed request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Missing or invalid bearer token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: The API key is read-only and cannot use this method
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Collection not found or RAG disabled
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "413":
          description: The request body is larger than this deployment accepts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "415":
          description: 'The request body was not sent with Content-Type: application/json'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "422":
          description: Invalid migration parameters
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "500":
          description: Database error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "503":
          description: Session authentication is temporarily unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/rag/collections/{name}/migrate-embeddings/{job_id}:
    get:
      tags:
        - rag
      summary: Get migration job status
      description: Returns the current status, progress details, or error information.
      operationId: migrate_status
      parameters:
        - name: name
          in: path
          description: Collection name
          required: true
          schema:
            type: string
        - name: job_id
          in: path
          description: Migration job ID (ULID)
          required: true
          schema:
            type: string
            maxLength: 26
            minLength: 26
            pattern: ^[0-9A-HJKMNP-TV-Za-hjkmnp-tv-z]{26}$
      responses:
        "200":
          description: Migration status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MigrateStatusResponse'
        "400":
          description: Malformed request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Missing or invalid bearer token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: An API key's project_id or x-project-id names a project other than the key's own
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Job not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "422":
          description: Request fails validation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "500":
          description: Database error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "503":
          description: Session authentication is temporarily unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/rag/collections/{name}/retrieve:
    post:
      tags:
        - rag
      summary: Retrieve relevance-ranked chunks from a RAG collection
      description: |-
        `mode` selects the retrieval strategy: `vector` (requires `query` or a
        caller-supplied `query_vector`), `full_text` (requires `query`, rejects
        `query_vector`), or `hybrid` (requires `query`, combining both signals).
        `top_k` (1..=200) bounds the returned chunk count; `candidate_multiplier`
        (1..=20), when set, widens the pre-rerank candidate pool. `501` when the
        collection's configured reranker is not available in this deployment.
        Requests wait for per-project capacity. A deployment with an external RAG
        backend may return `503` when that backend is unavailable and `504` when
        its transport deadline expires; an in-process deployment uses `503` when
        its retrieve limiter is unavailable.
      operationId: retrieve
      parameters:
        - name: name
          in: path
          description: Collection name
          required: true
          schema:
            type: string
          example: my-docs
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RetrieveRequest'
        required: true
      responses:
        "200":
          description: Retrieve completed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RetrieveResponse'
        "400":
          description: Malformed request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Missing or invalid bearer token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: The API key is read-only and cannot use this method
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Collection not found or RAG disabled
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "413":
          description: The request body is larger than this deployment accepts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "415":
          description: 'The request body was not sent with Content-Type: application/json'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "422":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "500":
          description: Database error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "501":
          description: Reranker kind not available in this deployment
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "503":
          description: RAG backend or in-process retrieve limiter unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "504":
          description: External RAG backend transport deadline exceeded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/rag/embedding-presets:
    get:
      tags:
        - rag
      summary: List available RAG embedding presets
      description: |-
        Returns the compile-time `xberg::embeddings::EMBEDDING_PRESETS` registry, capped at
        `MAX_MANAGED_EMBEDDING_PRESETS` entries, with each preset's `embedding_source` string and
        whether it is the server's default. The set is the same for every project and request.
      operationId: list_managed_embedding_presets
      responses:
        "200":
          description: Managed embedding presets
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListManagedEmbeddingPresetsResponse'
        "400":
          description: Invalid or conflicting project selector
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Missing or invalid bearer token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: Project access is forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: The current session has no membership in the requested project
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "500":
          description: Invalid server preset configuration
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "503":
          description: Session authentication is temporarily unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/rag/jobs/{job_id}:
    get:
      tags:
        - rag
      summary: Poll async RAG operation status
      description: |-
        Returns the current job state, operation type, and any available progress details.
        Status is determined by:
        - For ingest/reindex: document status in vector_documents (pending→indexed or failed)
        - For migrate: job status in NATS KV bucket (rag-migrate-status)

        Returns 404 if the job_id does not exist or belongs to another project (RLS enforcement).
      operationId: get_rag_job
      parameters:
        - name: job_id
          in: path
          description: Job ID (ULID)
          required: true
          schema:
            type: string
            maxLength: 26
            minLength: 26
            pattern: ^[0-9A-HJKMNP-TV-Za-hjkmnp-tv-z]{26}$
          example: 01ARZ3NDEKTSV4RRFFQ69G5FAV
      responses:
        "200":
          description: Job status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RagJobStatusResponse'
        "400":
          description: Malformed request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Missing or invalid bearer token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: An API key's project_id or x-project-id names a project other than the key's own
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Job not found or access denied
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "422":
          description: Invalid job ID
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "500":
          description: Persistence or dispatch error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "503":
          description: Session authentication is temporarily unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/saved_presets:
    get:
      tags:
        - saved-presets
      summary: List saved presets
      description: |-
        Returns a page of `SavedPresetSummary` entries — not the full schema each
        preset carries; fetch `GET /v1/saved_presets/{preset_id}` for that. `page` in the
        response is a zero-based page index derived from `offset / limit`, not an
        independent cursor.
      operationId: list_saved_presets
      parameters:
        - name: limit
          in: query
          description: Page size (default 50, accepted range 1..=100).
          required: false
          schema:
            type: integer
            format: int64
            default: 50
            maximum: 100
            minimum: 1
        - name: offset
          in: query
          description: Pagination offset (default 0, accepted range 0..=1,000,000).
          required: false
          schema:
            type: integer
            format: int64
            default: 0
            maximum: 1000000
            minimum: 0
      responses:
        "200":
          description: Saved presets for the current project
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListSavedPresetsResponse'
        "400":
          description: Malformed request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Missing or invalid bearer token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: An API key's project_id or x-project-id names a project other than the key's own
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: The current session has no membership in the requested project
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "422":
          description: Request fails validation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "500":
          description: Persistence error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "503":
          description: Session authentication is temporarily unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
    post:
      tags:
        - saved-presets
      summary: Create a saved preset
      description: |-
        A saved preset is a project's own reusable extraction configuration — a
        name, a JSON Schema (Draft 2020-12) the extraction output must satisfy,
        and a preferred call mode — distinct from the curated, cross-project
        preset registry served at `GET /v1/presets`. The schema is validated for
        well-formedness at creation time, not for reachability against any
        particular document. Returns `201` with `{success, id, name, message}`.
        Rejected with `403` for a read-only API key.
      operationId: create_saved_preset
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateSavedPresetRequest'
        required: true
      responses:
        "201":
          description: Preset created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateSavedPresetResponse'
        "400":
          description: Invalid request (bad name, schema, or call mode)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Missing or invalid bearer token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: The API key is read-only and cannot use this method
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: The current session has no membership in the requested project
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "409":
          description: A preset with this name already exists
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "413":
          description: The request body is larger than this deployment accepts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "415":
          description: 'The request body was not sent with Content-Type: application/json'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "422":
          description: Request fails validation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "500":
          description: Persistence error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "503":
          description: Session authentication is temporarily unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/saved_presets/{preset_id}:
    get:
      tags:
        - saved-presets
      summary: Get a saved preset
      description: |-
        `404` when `preset_id` does not name a preset in the caller's own project — a
        preset belonging to a different project is never visible, project
        membership is not checked separately.
      operationId: get_saved_preset
      parameters:
        - name: preset_id
          in: path
          description: Preset ID (UUID)
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Saved preset details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SavedPresetDetail'
        "400":
          description: Malformed request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Missing or invalid bearer token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: An API key's project_id or x-project-id names a project other than the key's own
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Preset not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "422":
          description: Request fails validation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "500":
          description: Persistence error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "503":
          description: Session authentication is temporarily unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
    delete:
      tags:
        - saved-presets
      summary: Delete a saved preset
      description: |-
        Irreversible: there is no soft-delete or recovery. `404` when `preset_id` does
        not name a preset in the caller's project. Rejected with `403` for a
        read-only API key.
      operationId: delete_saved_preset
      parameters:
        - name: preset_id
          in: path
          description: Preset ID (UUID)
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "204":
          description: Preset deleted
        "400":
          description: Malformed request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Missing or invalid bearer token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: The API key is read-only and cannot use this method
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Preset not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "422":
          description: Request fails validation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "500":
          description: Persistence error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "503":
          description: Session authentication is temporarily unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
    patch:
      tags:
        - saved-presets
      summary: Update a saved preset
      description: |-
        Omitted fields are preserved. Explicit null clears description, system prompt,
        or context template; non-nullable fields reject null. Provided fields are
        validated exactly as on create. `404` when `preset_id` does not name a preset in the
        caller's project. Rejected with `403` for a read-only API key.
      operationId: update_saved_preset
      parameters:
        - name: preset_id
          in: path
          description: Preset ID (UUID)
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateSavedPresetRequest'
        required: true
      responses:
        "200":
          description: Preset updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UpdateSavedPresetResponse'
        "400":
          description: Invalid request (bad name, schema, or call mode)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Missing or invalid bearer token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: The API key is read-only and cannot use this method
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Preset not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "409":
          description: A preset with this name already exists
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "413":
          description: The request body is larger than this deployment accepts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "415":
          description: 'The request body was not sent with Content-Type: application/json'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "422":
          description: Request fails validation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "500":
          description: Persistence error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "503":
          description: Session authentication is temporarily unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/tuning-profiles:
    get:
      tags:
        - auto-tune
      summary: List tuning profiles
      description: |-
        A tuning profile is an extraction configuration saved by
        `POST /v1/auto-tune/{id}/promote`. Returns a page of profile summaries,
        most recently created first; fetch `GET /v1/tuning-profiles/{id}` for one
        profile's full configuration.
      operationId: list_tuning_profiles
      parameters:
        - name: limit
          in: query
          description: Page size (default 50, accepted range 1..=100).
          required: false
          schema:
            type: integer
            format: int64
            default: 50
            maximum: 100
            minimum: 1
        - name: offset
          in: query
          description: Pagination offset (default 0, accepted range 0..=1,000,000).
          required: false
          schema:
            type: integer
            format: int64
            default: 0
            maximum: 1000000
            minimum: 0
      responses:
        "200":
          description: Tuning profiles for the current project
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListTuningProfilesResponse'
        "400":
          description: Malformed request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Missing or invalid authentication
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: An API key's project_id or x-project-id names a project other than the key's own
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Auto-tune is disabled or the project is unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "422":
          description: Request fails validation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "500":
          description: Persistence error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "503":
          description: Session authentication is temporarily unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/tuning-profiles/{id}:
    get:
      tags:
        - auto-tune
      summary: Get a tuning profile
      description: '`404` when `id` does not name a profile in the caller''s project.'
      operationId: get_tuning_profile
      parameters:
        - name: id
          in: path
          description: Tuning profile ID
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Tuning profile details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TuningProfileDetail'
        "400":
          description: Malformed request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Missing or invalid authentication
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: An API key's project_id or x-project-id names a project other than the key's own
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Tuning profile not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "422":
          description: Request fails validation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "500":
          description: Persistence error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "503":
          description: Session authentication is temporarily unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
    delete:
      tags:
        - auto-tune
      summary: Delete a tuning profile
      description: |-
        Irreversible: there is no soft-delete. Deleting a profile does not affect
        the auto-tune job that produced it, only the promoted profile record.
        `404` when `id` does not name a profile in the caller's project. Rejected
        with `403` for a read-only API key.
      operationId: delete_tuning_profile
      parameters:
        - name: id
          in: path
          description: Tuning profile ID
          required: true
          schema:
            type: string
      responses:
        "204":
          description: Tuning profile deleted
        "400":
          description: Malformed request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Missing or invalid authentication
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: The API key is read-only and cannot use this method
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Tuning profile not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "422":
          description: Request fails validation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "500":
          description: Persistence error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "503":
          description: Session authentication is temporarily unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/uploads/confirm:
    post:
      tags:
        - uploads
      summary: Confirm uploads and start processing
      description: Verifies all files exist in storage, then dispatches jobs to the worker queue.
      operationId: confirm_upload
      parameters:
        - name: X-Project-ID
          in: header
          description: Selected project; sessions require current membership, API keys remain scoped to their own project
          required: false
          schema:
            type: string
            format: uuid
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ConfirmUploadRequest'
        required: true
      responses:
        "202":
          description: Uploads confirmed, processing started
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConfirmUploadResponse'
              example:
                job_ids:
                  - 550e8400-e29b-41d4-a716-446655440000
                  - 550e8400-e29b-41d4-a716-446655440001
                status: processing
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: The API key is read-only and cannot use this method, or a stored batch asks for the managed LLM key (`structured_extraction.llm.use_managed`) on a project that is not entitled to it. In the second case no job is created; intake refuses the field, so only a batch stored before that refusal reaches this.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: llm_managed_keys_disabled
                  message: Managed LLM keys are not enabled for this project. Supply structured_extraction.llm.api_key, or contact support to enable managed keys.
        "404":
          description: Upload batch not found or expired
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "413":
          description: An uploaded document is larger than the project's `max_file_size_mb`
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "415":
          description: 'The request body was not sent with Content-Type: application/json'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "503":
          description: Session authentication is temporarily unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/uploads/presign:
    post:
      tags:
        - uploads
      summary: Generate presigned upload URLs
      description: |-
        Accepts up to 100 document metadata entries without carrying document bytes
        or enqueueing extraction. The cap bounds the pending-job fan-out created
        when the client calls `/v1/uploads/confirm`. The client uploads each file
        directly to storage before confirmation.
        <!-- public batch-limit contract -->
      operationId: presign_upload
      parameters:
        - name: X-Project-ID
          in: header
          description: Selected project; sessions require current membership, API keys remain scoped to their own project
          required: false
          schema:
            type: string
            format: uuid
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PresignUploadRequest'
        required: true
      responses:
        "200":
          description: Presigned upload URLs generated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PresignUploadResponse'
              example:
                batch_id: 550e8400-e29b-41d4-a716-446655440000
                uploads:
                  - job_id: 550e8400-e29b-41d4-a716-446655440000
                    upload_url: https://storage.googleapis.com/xberg-dev-uploads/...
                    object_key: projects/abc123/uploads/550e8400-e29b-41d4-a716-446655440000
                    method: PUT
                    expires_in_secs: 3600
                  - job_id: 550e8400-e29b-41d4-a716-446655440001
                    upload_url: https://storage.googleapis.com/xberg-dev-uploads/...
                    object_key: projects/abc123/uploads/550e8400-e29b-41d4-a716-446655440001
                    method: PUT
                    expires_in_secs: 3600
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: The API key is read-only and cannot use this method
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: The current session has no membership in the requested project
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "413":
          description: The request body is larger than this deployment accepts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "415":
          description: 'The request body was not sent with Content-Type: application/json'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "503":
          description: Session authentication is temporarily unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/usage:
    get:
      tags:
        - usage
      summary: Get usage statistics and quota
      description: |-
        Reports page and document counts, broken down by MIME type, for the
        window named by `start`/`end` (each `YYYY-MM-DD`); with both omitted the
        window defaults to the current calendar month. `start` after `end` is a
        `400`. `quota_limit`/`quota_remaining` are `null` when the project has no
        monthly page quota (unlimited).
      operationId: get_usage
      parameters:
        - name: start
          in: query
          description: Start date (ISO 8601, e.g. "2026-03-01"). Defaults to first day of current month.
          required: false
          schema:
            type: string
            format: date
        - name: end
          in: query
          description: Exclusive UTC end date (ISO 8601, e.g. "2026-04-01"). Defaults to first day of next month.
          required: false
          schema:
            type: string
            format: date
      responses:
        "200":
          description: Usage statistics
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UsageResponse'
              example:
                period_start: "2026-05-01"
                period_end: "2026-05-31"
                total_pages: 5432
                total_documents: 87
                total_failed: 2
                quota_limit: 100000
                quota_remaining: 94568
                by_mime_type:
                  application/pdf:
                    documents: 65
                    pages: 3200
                    failed: 1
                  image/png:
                    documents: 15
                    pages: 1800
                    failed: 0
                  text/plain:
                    documents: 7
                    pages: 432
                    failed: 1
        "400":
          description: Invalid date format
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: An API key's project_id or x-project-id names a project other than the key's own
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: The current session has no membership in the requested project
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "503":
          description: Session authentication is temporarily unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/webhooks/{webhook_id}/deliveries:
    get:
      tags:
        - webhooks
      summary: List delivery metadata for a subscription belonging to the authenticated project.
      description: Returns retained attempt metadata without request or response payload previews, scoped to the authenticated project and paginated by limit and offset.
      operationId: list_webhook_deliveries
      parameters:
        - name: webhook_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: limit
          in: query
          description: |-
            Page size. A value outside 1..=1000 is rejected with a 400, on both
            tiers.
          required: false
          schema:
            type: integer
            format: int64
            default: 100
            maximum: 1000
            minimum: 1
          example: 100
        - name: offset
          in: query
          description: |-
            Number of items to skip. A negative value is rejected with a 400; a
            value above 1,000,000 is accepted but clamped to 1,000,000, which
            selects an empty page rather than erroring.
          required: false
          schema:
            type: integer
            format: int64
            default: 0
            maximum: 1000000
            minimum: 0
          example: 0
      responses:
        "200":
          description: Delivery metadata without payload bodies
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListWebhookDeliveriesResponse'
        "400":
          description: Invalid path or pagination
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: An API key's project_id or x-project-id names a project other than the key's own
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Webhook not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "500":
          description: History unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "503":
          description: Session authentication is temporarily unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/webhooks/{webhook_id}/deliveries/{delivery_id}:
    get:
      tags:
        - webhooks
      summary: Read bounded request and response previews, which may contain project document content.
      description: Returns one attempt with bounded request and response previews for a webhook in the authenticated project; truncation flags identify incomplete previews.
      operationId: get_webhook_delivery
      parameters:
        - name: webhook_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: delivery_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Delivery details and bounded payload previews
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookDeliveryDetailResponse'
        "400":
          description: Invalid path
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: An API key's project_id or x-project-id names a project other than the key's own
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Webhook or delivery not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "500":
          description: History unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "503":
          description: Session authentication is temporarily unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
components:
  schemas:
    AnnotationKind:
      oneOf:
        - type: object
          description: Bold (strong) text formatting.
          required:
            - annotation_type
          properties:
            annotation_type:
              type: string
              enum:
                - bold
        - type: object
          description: Italic (emphasis) text formatting.
          required:
            - annotation_type
          properties:
            annotation_type:
              type: string
              enum:
                - italic
        - type: object
          description: Underlined text.
          required:
            - annotation_type
          properties:
            annotation_type:
              type: string
              enum:
                - underline
        - type: object
          description: Strikethrough text.
          required:
            - annotation_type
          properties:
            annotation_type:
              type: string
              enum:
                - strikethrough
        - type: object
          description: Inline code span.
          required:
            - annotation_type
          properties:
            annotation_type:
              type: string
              enum:
                - code
        - type: object
          description: Subscript text.
          required:
            - annotation_type
          properties:
            annotation_type:
              type: string
              enum:
                - subscript
        - type: object
          description: Superscript text.
          required:
            - annotation_type
          properties:
            annotation_type:
              type: string
              enum:
                - superscript
        - type: object
          description: Hyperlink annotation.
          required:
            - url
            - annotation_type
          properties:
            annotation_type:
              type: string
              enum:
                - link
            title:
              type:
                - string
                - "null"
              description: Optional link title attribute.
            url:
              type: string
              description: Hyperlink target URL.
        - type: object
          description: Highlighted text (PDF highlights, HTML `<mark>`).
          required:
            - annotation_type
          properties:
            annotation_type:
              type: string
              enum:
                - highlight
        - type: object
          description: Text color (CSS-compatible value, e.g. "#ff0000", "red").
          required:
            - value
            - annotation_type
          properties:
            annotation_type:
              type: string
              enum:
                - color
            value:
              type: string
              description: CSS-compatible color value (e.g. `"#ff0000"`, `"red"`).
        - type: object
          description: Font size with units (e.g. "12pt", "1.2em", "16px").
          required:
            - value
            - annotation_type
          properties:
            annotation_type:
              type: string
              enum:
                - font_size
            value:
              type: string
              description: Font size including unit (e.g. `"12pt"`, `"1.2em"`, `"16px"`).
        - type: object
          description: Extensible annotation for format-specific styling.
          required:
            - name
            - annotation_type
          properties:
            annotation_type:
              type: string
              enum:
                - custom
            name:
              type: string
              description: Name of the custom annotation kind.
            value:
              type:
                - string
                - "null"
              description: Optional value or parameter for the annotation.
      description: Types of inline text annotations.
    ArchiveEntry:
      type: object
      description: |-
        A single file extracted from an archive.

        When archives (ZIP, TAR, 7Z, GZIP) are extracted with recursive extraction
        enabled, each processable file produces its own full `ExtractedDocument`.
      required:
        - path
        - mime_type
        - result
      properties:
        mime_type:
          type: string
          description: Detected MIME type of the file.
        path:
          type: string
          description: Archive-relative file path (e.g. "folder/document.pdf").
        result:
          $ref: '#/components/schemas/ExtractedDocument'
          description: Full extraction result for this file.
    ArchiveMetadata:
      type: object
      description: |-
        Archive (ZIP/TAR/7Z) metadata.

        Extracted from compressed archive files containing file lists and size information.
      required:
        - format
        - file_count
        - file_list
        - total_size
        - format_type
      properties:
        compressed_size:
          type:
            - integer
            - "null"
          format: int64
          description: Compressed size in bytes (if available)
          minimum: 0
        file_count:
          type: integer
          format: int32
          description: Total number of files in the archive
          minimum: 0
        file_list:
          type: array
          items:
            type: string
          description: List of file paths within the archive
        format:
          type: string
          description: Archive format ("ZIP", "TAR", "7Z", etc.)
        format_type:
          type: string
          enum:
            - archive
        total_size:
          type: integer
          format: int64
          description: Total uncompressed size in bytes
          minimum: 0
    Attributes:
      type: object
      description: |-
        Element attributes in Djot.

        Represents the attributes attached to elements using {.class #id key="value"} syntax.
      properties:
        classes:
          type: array
          items:
            type: string
          description: CSS classes (.class1 .class2)
        id:
          type:
            - string
            - "null"
          description: Element ID (#identifier)
        key_values:
          type: array
          items:
            $ref: '#/components/schemas/KeyValueAttribute'
          description: Key-value pairs (key="value")
    AudioMetadata:
      type: object
      description: |-
        Audio/video file metadata.

        Populated from container tags (ID3v2, MP4 atoms, Vorbis comments, etc.) and
        PCM decode properties. Available when the `transcription-types` feature is enabled.
      required:
        - format_type
      properties:
        bitrate:
          type:
            - integer
            - "null"
          format: int32
          description: Audio bitrate in kbps from the source file tags/properties.
          minimum: 0
        channels:
          type:
            - integer
            - "null"
          format: int32
          description: Number of audio channels (1 = mono, 2 = stereo).
          minimum: 0
        codec:
          type:
            - string
            - "null"
          description: Audio codec (e.g. "mp3", "aac", "opus", "flac").
        container:
          type:
            - string
            - "null"
          description: Container format (e.g. "mpeg", "mp4", "ogg", "wav").
        duration_ms:
          type:
            - integer
            - "null"
          format: int64
          description: Duration in milliseconds derived from the decoded audio stream.
          minimum: 0
        format_type:
          type: string
          enum:
            - audio
        sample_rate_hz:
          type:
            - integer
            - "null"
          format: int32
          description: Sample rate in Hz after decode (always 16000 when resampled for Whisper).
          minimum: 0
    AuditEntry:
      type: object
      description: A single customer-facing audit log entry.
      required:
        - id
        - actor
        - action
        - resource_type
        - metadata
        - created_at
      properties:
        action:
          type: string
          description: Action performed (e.g. `"job.submit"`, `"api_key.revoke"`).
        actor:
          type: string
          description: Actor that performed the action (e.g. an API key prefix or user id).
        created_at:
          type: string
          format: date-time
          description: ISO 8601 event timestamp.
        id:
          type: string
          format: uuid
          description: Unique event identifier.
        metadata:
          type: object
          description: Additional structured metadata describing the event.
          additionalProperties: {}
          propertyNames:
            type: string
        resource_id:
          type:
            - string
            - "null"
          description: Identifier of the affected resource, when applicable.
        resource_type:
          type: string
          description: Type of resource the action applied to (e.g. `"job"`, `"api_key"`).
    AutoTuneBudget:
      type: object
      description: Budget limits for an auto-tune run.
      properties:
        max_billable_pages:
          type:
            - integer
            - "null"
          format: int64
          description: Maximum number of billable pages to process across all trials.
          minimum: 0
        max_trials:
          type:
            - integer
            - "null"
          format: int32
          description: Maximum number of trials to run.
          minimum: 0
        max_wall_clock_secs:
          type:
            - integer
            - "null"
          format: int64
          description: Maximum wall-clock time, in seconds, before the run is cut short.
          minimum: 0
    AutoTuneCapabilitiesResponse:
      type: object
      description: |-
        Auto-tune capabilities (OCR backends and tunable knobs) reported for a deployment. Powers a
        deployment-aware wizard rather than hardcoding backend/knob names on the frontend.
      required:
        - max_trials
        - max_wall_clock_secs
        - ocr_backends
        - default_ocr_ladder
        - tunable_knobs
      properties:
        default_ocr_ladder:
          type: array
          items:
            type: string
          description: Backend names composing the default OCR search ladder, in search order.
        max_trials:
          type: integer
          format: int32
          description: Maximum number of trials accepted for one run by this deployment.
          minimum: 0
        max_wall_clock_secs:
          type: integer
          format: int64
          description: Maximum wall-clock budget accepted for one run, in seconds.
          minimum: 0
        ocr_backends:
          type: array
          items:
            $ref: '#/components/schemas/OcrBackendCapabilityInfo'
          description: OCR backends available to this deployment.
        structured_ground_truth_available:
          type: boolean
          description: Whether the deployment can score structured ground truth with its configured language model.
        tunable_knobs:
          type: array
          items:
            $ref: '#/components/schemas/TunableKnobInfo'
          description: Knobs that auto-tune can search over.
    AutoTuneDocumentInput:
      type: object
      description: One document submitted to an auto-tune run, paired with its ground truth.
      required:
        - filename
        - ground_truth_kind
        - ground_truth
      properties:
        content_ref:
          type:
            - string
            - "null"
          description: |-
            Reserved, not yet supported: a request that sets this field is rejected with 400 on every
            tier (no backend resolves it yet). Upload the document bytes as a multipart file part instead.
            Intended future use: referencing previously stored bytes by a per-project storage key without
            re-uploading them.
          readOnly: true
        filename:
          type: string
          description: Original filename of the document (used for format detection).
        ground_truth:
          type: string
          description: |-
            The ground truth payload. For `markdown`/`plaintext`, this is the
            raw expected text. For `structured`, this is a JSON string encoding
            `{"schema": ..., "expected": ...}` (both the JSON schema and the
            expected output are required).
        ground_truth_kind:
          $ref: '#/components/schemas/GroundTruthKind'
          description: Which kind of ground truth is supplied.
    AutoTuneHealth:
      type: object
      description: |-
        Auto-tune readiness summary for the health response. Enterprise has no deploy-time
        toggle for auto-tune (see `crate::autotune::config`) — the `/v1/auto-tune` and
        `/v1/tuning-profiles` routes always mount, so this unconditionally reports `true`;
        access is controlled per-request by the project's billing entitlement instead. Mirrors
        `services/pro`'s `auto_tune.enabled` health field so the frontend's `TierContext` reads
        one wire contract across both products.
      required:
        - enabled
      properties:
        enabled:
          type: boolean
          description: Always `true` for Enterprise — see the struct docs.
          example: true
    AutoTuneJobStatus:
      type: object
      description: Current status and progress of an auto-tune job.
      required:
        - auto_tune_job_id
        - status
        - trials_completed
        - trials_total
      properties:
        auto_tune_job_id:
          type: string
          description: Unique ID of the auto-tune job (ULID).
        best_score:
          type:
            - number
            - "null"
          format: double
          description: Best primary score observed so far, if any trial has completed.
        budget_spent:
          type:
            - integer
            - "null"
          format: int64
          description: |-
            Billable pages metered across this run's terminal trials on Enterprise, including completed,
            partially successful, failed and cancelled trials. In-flight trials are excluded.
            Zero before any terminal trial is metered; null on Pro, where auto-tune trial-page spending is unavailable.
          minimum: 0
        error:
          type:
            - string
            - "null"
          description: Why the run failed. Set only when `status` is `failed`, and null otherwise.
        status:
          type: string
          description: Current job status (e.g. `queued`, `running`, `completed`, `failed`).
        trials_completed:
          type: integer
          format: int32
          description: Number of trials completed so far.
          minimum: 0
        trials_total:
          type: integer
          format: int32
          description: Total number of trials planned for this run.
          minimum: 0
    AutoTuneJobSummary:
      type: object
      description: Lightweight projection of an auto-tune job (for listings).
      required:
        - auto_tune_job_id
        - status
        - trials_completed
        - trials_total
        - created_at
      properties:
        auto_tune_job_id:
          type: string
          description: Unique ID of the auto-tune job (ULID).
        best_score:
          type:
            - number
            - "null"
          format: double
          description: Best primary score observed, if any trial has completed.
        created_at:
          type: string
          format: date-time
          description: ISO 8601 creation timestamp.
        status:
          type: string
          description: Current job status (`running`, `completed`, or `failed`).
        trials_completed:
          type: integer
          format: int32
          description: Number of trials completed so far.
          minimum: 0
        trials_total:
          type: integer
          format: int32
          description: Total number of trials planned for this run.
          minimum: 0
    AutoTuneMultipartForm:
      type: object
      description: |-
        Multipart (`multipart/form-data`) variant of the submit-auto-tune request body.

        Send a text part named `request` carrying a JSON-encoded
        `CreateAutoTuneJobRequest`, plus one binary part per document whose
        part name (or filename) matches a `documents[].filename` entry in the
        request.
      required:
        - request
        - file
      properties:
        file:
          type: array
          items:
            type: integer
            format: binary
            minimum: 0
          description: A document file. Repeat with one binary part per file.
          contentMediaType: application/octet-stream
        request:
          $ref: '#/components/schemas/CreateAutoTuneJobRequest'
          description: The JSON-encoded `request` part — a `CreateAutoTuneJobRequest`.
    AutoTuneResult:
      type: object
      description: Final result of a completed auto-tune run.
      required:
        - auto_tune_job_id
        - profile
        - leaderboard
      properties:
        auto_tune_job_id:
          type: string
          description: Unique ID of the auto-tune job (ULID).
        held_out_confidence:
          type:
            - number
            - "null"
          format: double
          description: Gain from routing over the global configuration on held-out documents.
        leaderboard:
          type: array
          items:
            $ref: '#/components/schemas/LeaderboardEntry'
          description: Ranked leaderboard of trialed configurations.
        profile:
          type: object
          description: |-
            The winning tuning profile artifact. The concrete `TuningProfile`
            shape is defined in `autotune-core`; here it is opaque JSON.
        routing_outcome:
          $ref: '#/components/schemas/RoutingOutcome'
          description: |-
            Whether routing was requested, kept routes, or fell back to the global configuration.
            Absent on results recorded before this field was introduced.
        search_space_total:
          type:
            - integer
            - "null"
          format: int64
          description: |-
            Combinations the search space held. Compare it with the leaderboard
            length to see how much of the space the trial budget reached: a run
            that explored a fraction of it otherwise reads like one that explored
            all of it. Absent on results recorded before this was reported.
          minimum: 0
    AutoTuneSearchConfig:
      type: object
      description: Search configuration for an auto-tune run.
      required:
        - search_space
      properties:
        max_trials:
          type:
            - integer
            - "null"
          format: int32
          description: Maximum number of trials to run during the search.
          minimum: 0
        search_params:
          $ref: '#/components/schemas/AutoTuneSearchParams'
          description: |-
            Structured, wizard-friendly alternative to `search_space`. Ignored when `search_space` is
            also set.
        search_space:
          type: object
          description: |-
            Which knobs/axes to tune, and their candidate values. The concrete
            `SearchSpace` shape is defined in `autotune-core`; here it is
            opaque JSON so this crate has no dependency on that crate.

            Power-user escape hatch. Takes precedence over `search_params` when both are set.
        tolerance:
          type:
            - number
            - "null"
          format: double
          description: Tolerance band used when ranking near-tied configurations.
          maximum: 1
          minimum: 0
    AutoTuneSearchParams:
      type: object
      description: |-
        Wizard-friendly, structured alternative to `AutoTuneSearchConfig.search_space`: which OCR
        backends to sweep and per-knob candidate values, without a client having to construct a full
        `autotune_core::search::SearchSpace`.
      required:
        - candidates
      properties:
        candidates:
          type: object
          description: |-
            Per-knob candidate-value overrides, keyed by the knob's wire name (see
            `TunableKnobInfo.knob` / `autotune_core::search::Knob` for the full set of names). Each
            knob's values are opaque JSON so this crate has no dependency on `autotune-core`.
          additionalProperties:
            type: array
            items: {}
          propertyNames:
            type: string
        ocr_backends:
          type:
            - array
            - "null"
          items:
            type: string
          description: |-
            OCR backends to sweep, in dominance order. `None` falls back to the deployment's default
            OCR ladder (see `AutoTuneCapabilitiesResponse.default_ocr_ladder`).
    AutoTuneWebhookEvent:
      type: object
      description: |-
        Auto-Tune terminal body. Retrieve the full result through
        `GET /v1/auto-tune/{id}/result`; webhook retries carry this same small body.
      required:
        - event
        - event_id
        - project_id
        - auto_tune_job_id
        - status
        - timestamp
        - trials_completed
        - trials_total
      properties:
        auto_tune_job_id:
          type: string
        best_score:
          type:
            - number
            - "null"
          format: double
        error:
          $ref: '#/components/schemas/WebhookEventError'
        event:
          $ref: '#/components/schemas/AutoTuneWebhookEventType'
        event_id:
          type: string
          format: uuid
        metadata:
          type: object
          additionalProperties:
            type: string
          propertyNames:
            type: string
        project_id:
          type: string
          format: uuid
        status:
          $ref: '#/components/schemas/AutoTuneWebhookStatus'
        timestamp:
          type: string
          format: date-time
        trials_completed:
          type: integer
          format: int32
          minimum: 0
        trials_total:
          type: integer
          format: int32
          minimum: 0
    AutoTuneWebhookEventType:
      type: string
      description: Terminal event names for Auto-Tune runs, separate from extraction-job events.
      enum:
        - auto_tune.completed
        - auto_tune.failed
    AutoTuneWebhookStatus:
      type: string
      enum:
        - completed
        - failed
    BibtexMetadata:
      type: object
      description: BibTeX bibliography metadata.
      required:
        - entry_count
        - format_type
      properties:
        authors:
          type: array
          items:
            type: string
          description: Author names collected across all bibliography entries.
        citation_keys:
          type: array
          items:
            type: string
          description: BibTeX citation keys (e.g. `"knuth1984"`) for all entries.
        entry_count:
          type: integer
          description: Number of entries in the bibliography.
          minimum: 0
        entry_types:
          type:
            - object
            - "null"
          description: Count of entries grouped by BibTeX entry type (e.g. `"article"` → 5).
          additionalProperties:
            type: integer
            minimum: 0
          propertyNames:
            type: string
        format_type:
          type: string
          enum:
            - bibtex
        year_range:
          $ref: '#/components/schemas/YearRange'
          description: Earliest and latest publication years found in the bibliography.
    BlockType:
      type: string
      description: Types of block-level elements in Djot.
      enum:
        - paragraph
        - heading
        - blockquote
        - code_block
        - list_item
        - ordered_list
        - bullet_list
        - task_list
        - definition_list
        - definition_term
        - definition_description
        - div
        - section
        - thematic_break
        - raw_block
        - math_display
    BoundingBox:
      type: object
      description: Bounding box coordinates for element positioning.
      required:
        - x0
        - y0
        - x1
        - y1
      properties:
        x0:
          type: number
          format: double
          description: Left x-coordinate
        x1:
          type: number
          format: double
          description: Right x-coordinate
        y0:
          type: number
          format: double
          description: Bottom y-coordinate
        y1:
          type: number
          format: double
          description: Top y-coordinate
    BrowserOverrides:
      type: object
      description: |-
        Browser rendering overrides for a specific crawl request.

        The browser tier is supported. Seed URLs are SSRF-checked on every path, so
        the browser carries no exemption; `bypass` and `proxy` are the tiers that
        remain unavailable, and setting either returns HTTP 400.
      properties:
        backend:
          type:
            - string
            - "null"
          description: Browser backend. One of `native` or `chromiumoxide`.
          example: native
        mode:
          type:
            - string
            - "null"
          description: Browser mode. One of `auto`, `always` or `never`.
          example: never
        stealth:
          type:
            - boolean
            - "null"
          description: |-
            Route every request through the browser's stealth tier (JS stealth
            patches, TLS fingerprint spoofing, stealth user agent). Works on both
            backends; cannot combine with `mode: "never"`. An explicit `false`
            opts out of stealth even when the crawl pod defaults to it; an
            explicit `mode` keeps precedence either way.
          example: false
    BypassMode:
      type: string
      description: |-
        Bypass mode — determines whether cloud provides credentials (managed) or
        the customer does (bring-your-own).
      enum:
        - managed
        - byo
    BypassRequestOverride:
      type: object
      description: |-
        Per-request bypass override. Bypass execution is unavailable pending
        connect-time DNS pinning; requests that set this object return HTTP 400.
      required:
        - mode
        - vendor
      properties:
        credentials:
          type: object
          description: Reserved vendor credentials; bypass execution is unavailable.
        mode:
          $ref: '#/components/schemas/BypassMode'
          description: Whether cloud provides vendor credentials (managed) or caller does (byo).
        vendor:
          type: string
          description: Reserved vendor identifier; bypass execution is unavailable.
    CellChange:
      type: object
      description: |-
        A single changed cell within a table.

        Defined here (rather than only in `crate::diff`) so `RevisionDelta` can
        reference it unconditionally, without requiring the `diff` Cargo feature.
        `crate::diff` re-exports this type verbatim.
      required:
        - row
        - col
        - from
        - to
      properties:
        col:
          type: integer
          description: Zero-based column index.
          minimum: 0
        from:
          type: string
          description: Value before the change.
        row:
          type: integer
          description: Zero-based row index.
          minimum: 0
        to:
          type: string
          description: Value after the change.
    Chunk:
      type: object
      description: |-
        A text chunk with optional embedding and metadata.

        Chunks are created when chunking is enabled in `ExtractionConfig`. Each chunk
        contains the text content, optional embedding vector (if embedding generation
        is configured), and metadata about its position in the document.
      required:
        - content
        - metadata
      properties:
        chunk_type:
          $ref: '#/components/schemas/ChunkType'
          description: |-
            Semantic structural classification of this chunk.

            Assigned by the heuristic classifier based on content patterns and
            heading context. Defaults to `ChunkType::Unknown` when no rule matches.
        content:
          type: string
          description: The text content of this chunk.
        embedding:
          type:
            - array
            - "null"
          items:
            type: number
            format: float
          description: |-
            Optional embedding vector for this chunk.

            Only populated when `EmbeddingConfig` is provided in chunking configuration.
            The dimensionality depends on the chosen embedding model.
        late_interaction:
          $ref: '#/components/schemas/MultiVectorEmbedding'
          description: |-
            Optional ColBERT-style multi-vector (late-interaction) embedding for this chunk.

            Only populated when late-interaction embedding generation is configured for
            chunking. `None` otherwise, including on builds without the `late-interaction`
            feature.

            Uses the crate-root `MultiVectorEmbedding` alias for the same reason
            `sparse_embedding` uses `SparseEmbedding` — see that field's docs.
        metadata:
          $ref: '#/components/schemas/ChunkMetadata'
          description: Metadata about this chunk's position and properties.
        sparse_embedding:
          $ref: '#/components/schemas/SparseEmbedding'
          description: |-
            Optional sparse (SPLADE) learned embedding for this chunk.

            Only populated when sparse-embedding generation is configured for chunking.
            `None` otherwise, including on builds without the `sparse-embeddings` feature.

            Uses the crate-root `SparseEmbedding` alias rather than
            `crate::sparse_embeddings::SparseEmbedding` directly: the `sparse_embeddings`
            module itself only compiles under `sparse-embeddings`/`sparse-embedding-presets`,
            while the crate-root alias is always defined (a field-compatible stub on builds
            without either feature), so this field — and `Chunk` itself — compiles on every
            feature combination, including the crate's default features.
    ChunkMetadata:
      type: object
      description: Metadata about a chunk's position in the original document.
      required:
        - byte_start
        - byte_end
        - chunk_index
        - total_chunks
      properties:
        byte_end:
          type: integer
          description: Byte offset where this chunk ends in the original text (UTF-8 valid boundary).
          minimum: 0
        byte_start:
          type: integer
          description: Byte offset where this chunk starts in the original text (UTF-8 valid boundary).
          minimum: 0
        chunk_index:
          type: integer
          description: Zero-based index of this chunk in the document.
          minimum: 0
        classifications:
          type: array
          items:
            $ref: '#/components/schemas/ClassificationLabel'
          description: |-
            Multi-label classification result for this chunk.

            Populated by the chunk-classification post-processor when
            `ExtractionConfig.chunk_classification`
            is set. A chunk may match zero, one, or many of the configured label
            definitions. Empty when chunk classification was not configured.
        first_page:
          type:
            - integer
            - "null"
          format: int32
          description: |-
            First page number this chunk spans (1-indexed).

            Only populated when page tracking is enabled in extraction configuration.
          minimum: 0
        heading_context:
          $ref: '#/components/schemas/HeadingContext'
          description: |-
            Heading context when using Markdown chunker.

            Contains the heading hierarchy this chunk falls under.
            Only populated when `ChunkerType::Markdown` is used.
        heading_path:
          type: array
          items:
            type: string
          description: |-
            Flattened heading trail from document root to this chunk's section.

            Each element is a heading's text, outermost first. Derived from
            `heading_context` when present; empty otherwise.
            Provides a binding-friendly, RAG-shaped breadcrumb without requiring
            callers to walk the nested `HeadingContext` structure.
        image_indices:
          type: array
          items:
            type: integer
            format: int32
            minimum: 0
          description: |-
            Indices into `ExtractedDocument.images` for images on pages covered by this chunk.

            Contains zero-based indices into the top-level `images` collection for every
            image whose `page_number` falls within `[first_page, last_page]`.
            Empty when image extraction is disabled or the chunk spans no pages with images.
        last_page:
          type:
            - integer
            - "null"
          format: int32
          description: |-
            Last page number this chunk spans (1-indexed, equal to first_page for single-page chunks).

            Only populated when page tracking is enabled in extraction configuration.
          minimum: 0
        node_ids:
          type: array
          items:
            type: string
          description: |-
            Ids of the `DocumentNode`s
            this chunk was derived from.

            Joins a chunk back to the structured document tree via
            `DocumentNode.id`.
            Populated from exact node provenance when available, with a textual
            containment fallback for rendered chunks that do not retain byte offsets.
        page_spans:
          type: array
          items:
            $ref: '#/components/schemas/PageSpan'
          description: |-
            Per-page bounding-box spans this chunk covers, for viewer highlighting (#1295).

            One entry per page the chunk overlaps, in page order — the first and last entries'
            `page` fields equal `first_page`/`last_page`.
            Populated whenever page-boundary provenance is available (the same condition under
            which `first_page`/`last_page` are populated); each entry's `bbox` is additionally
            populated when the document's structured node tree (`ExtractedDocument.document`) is
            available, as the union of that page's body-layer node bounding boxes found within this
            chunk. Empty when page-boundary provenance is unavailable (mirrors `first_page`/
            `last_page` being `None`).
        token_count:
          type:
            - integer
            - "null"
          description: |-
            Number of tokens in this chunk (if available).

            This is calculated by the embedding model's tokenizer if embeddings are enabled.
          minimum: 0
        total_chunks:
          type: integer
          description: Total number of chunks in the document.
          minimum: 0
    ChunkType:
      type: string
      description: |-
        Semantic structural classification of a text chunk.

        Assigned by the heuristic classifier in `chunking::classifier`.
        Defaults to `Unknown` when no rule matches.
        Designed to be extended in future versions without breaking changes.
      enum:
        - heading
        - party_list
        - definitions
        - operative_clause
        - signature_block
        - schedule
        - table_like
        - formula
        - code_block
        - function
        - class
        - module
        - image
        - org_chart
        - diagram
        - unknown
    ChunkingConfig:
      type: object
      description: Text chunking configuration.
      properties:
        chunker_type:
          type:
            - string
            - "null"
          description: |-
            Chunker strategy: `text`, `markdown`, `yaml`, or `semantic`. Default
            `text`. Any other value is refused with a 400.
        embedding:
          $ref: '#/components/schemas/EmbeddingConfig'
          description: Embedding generation configuration
        max_characters:
          type:
            - integer
            - "null"
          format: int32
          description: Maximum chunk size in characters. Defaults to 1000 when omitted.
          minimum: 0
        overlap:
          type:
            - integer
            - "null"
          format: int32
          description: |-
            Overlap between adjacent chunks in characters. Defaults to 200 when omitted,
            reduced to fit when `max_characters` is smaller than that.
          minimum: 0
        preset:
          type:
            - string
            - "null"
          description: Preset name (overrides other settings)
        trim:
          type:
            - boolean
            - "null"
          description: Trim whitespace from chunk boundaries. Defaults to true when omitted.
      additionalProperties: false
    CitationMetadata:
      type: object
      description: Citation file metadata (RIS, PubMed, EndNote).
      required:
        - citation_count
        - format_type
      properties:
        authors:
          type: array
          items:
            type: string
          description: Author names collected across all citation records.
        citation_count:
          type: integer
          description: Total number of citation records in the file.
          minimum: 0
        dois:
          type: array
          items:
            type: string
          description: DOI identifiers found in the citation records.
        format:
          type:
            - string
            - "null"
          description: Detected citation file format (e.g. `"ris"`, `"pubmed"`, `"endnote"`).
        format_type:
          type: string
          enum:
            - citation
        keywords:
          type: array
          items:
            type: string
          description: Keywords collected from all citation records.
        year_range:
          $ref: '#/components/schemas/YearRange'
          description: Earliest and latest publication years found in the file.
    ClassificationLabel:
      type: object
      description: A single label + confidence pair.
      required:
        - label
      properties:
        confidence:
          type:
            - number
            - "null"
          format: float
          description: |-
            Backend-reported confidence in `[0.0, 1.0]`. `None` when the backend (e.g. an LLM
            prompt without explicit confidence schema) did not report one.
        label:
          type: string
          description: Label name as configured in `PageClassificationConfig::labels`.
    CodeChunkInfo:
      type: object
      description: |-
        A single structurally-meaningful code chunk produced by tree-sitter parsing.

        Purpose-built payload owned by xberg — deliberately does not expose the upstream
        `tree_sitter_language_pack` types, so binding generators never need to resolve an
        external crate's types across FFI/language boundaries.
      required:
        - text
        - context_path
        - node_types
        - byte_start
        - byte_end
      properties:
        byte_end:
          type: integer
          description: Exclusive end byte offset of this chunk in the original source.
          minimum: 0
        byte_start:
          type: integer
          description: Inclusive start byte offset of this chunk in the original source.
          minimum: 0
        context_path:
          type: array
          items:
            type: string
          description: Hierarchical path of enclosing structural items (e.g. `["MyClass", "my_method"]`).
        node_types:
          type: array
          items:
            type: string
          description: |-
            Tree-sitter node kinds that appear at the top level of this chunk (e.g.
            `"function_definition"`, `"class_definition"`).
        text:
          type: string
          description: The raw source text of this chunk.
    CodeDataAttribute:
      type: object
      description: |-
        An XML-style attribute attached to an `Element` node.

        Populated only for `CodeDataNodeKind::Element`; always empty for `KeyValue` and
        `Sequence` nodes.
      required:
        - name
        - value
        - byte_start
        - byte_end
      properties:
        byte_end:
          type: integer
          description: Exclusive end byte offset of the `name="value"` attribute token.
          minimum: 0
        byte_start:
          type: integer
          description: Inclusive start byte offset of the `name="value"` attribute token.
          minimum: 0
        name:
          type: string
          description: Attribute name (e.g. `"class"`, `"href"`).
        value:
          type: string
          description: Attribute value as a raw string (quotes stripped).
    CodeDataNode:
      type: object
      description: |-
        A node in the hierarchical data tree produced by data-format extraction.

        Purpose-built payload owned by xberg — mirrors
        `tree_sitter_language_pack::DataNode` but flattens its `Span` down to plain byte
        offsets, so binding generators never need to resolve an external crate's types
        across FFI/language boundaries.
      required:
        - kind
        - byte_start
        - byte_end
      properties:
        attributes:
          type: array
          items:
            $ref: '#/components/schemas/CodeDataAttribute'
          description: |-
            Attributes on element-shape nodes (XML `STag` attributes). Empty for all
            other kinds.
        byte_end:
          type: integer
          description: Exclusive end byte offset of this node in the original source.
          minimum: 0
        byte_start:
          type: integer
          description: Inclusive start byte offset of this node in the original source.
          minimum: 0
        children:
          type: array
          items:
            $ref: '#/components/schemas/CodeDataNode'
          description: Children for nested containers and XML element bodies.
        key:
          type:
            - string
            - "null"
          description: |-
            Key, attribute name, tag name, or positional index (`"0"`, `"1"`, …).
            `None` at the document root.
        kind:
          $ref: '#/components/schemas/CodeDataNodeKind'
          description: Whether this node is a key/value pair, XML element, or sequence item.
        value:
          type:
            - string
            - "null"
          description: |-
            Leaf scalar value, if any. `None` for containers (objects, arrays, XML
            elements with child elements).
    CodeDataNodeKind:
      type: string
      description: |-
        Discriminates the shape of a `CodeDataNode`.

        Purpose-built mirror of `tree_sitter_language_pack::DataNodeKind` — kept as an
        xberg-owned type so binding generators never need to resolve the upstream crate's
        types across FFI/language boundaries.
      enum:
        - key_value
        - element
        - sequence
    CodeMetadata:
      type: object
      description: |-
        Code-format metadata: the structural chunks produced by tree-sitter parsing.

        Wrapped by `FormatMetadata.Code`. Kept as a named struct (rather than an inline
        enum-variant body) so serde can tag it under internal tagging and utoipa can emit a
        referenceable `CodeMetadata` component in the OpenAPI schema.
      required:
        - chunks
        - format_type
      properties:
        chunks:
          type: array
          items:
            $ref: '#/components/schemas/CodeChunkInfo'
          description: Structural code chunks (function/class/module boundaries).
        data:
          $ref: '#/components/schemas/CodeDataNode'
          description: |-
            Hierarchical key/value data tree extracted from data-format source
            (JSON, YAML, TOML, XML, CSV, etc.), when data extraction was enabled.
        format_type:
          type: string
          enum:
            - code
    CollectionChunkingConfig:
      type: object
      description: |-
        Per-collection text chunking configuration.

        Mirrors the tunable knobs of xberg's chunker. Every field is optional;
        omitted fields fall back to the ingest pipeline's defaults. The API layer
        converts this into xberg's native `ChunkingConfig` before persisting it
        to the collection, so the ingest pipeline consumes the canonical type.
      properties:
        chunker_type:
          type:
            - string
            - "null"
          description: 'Chunker strategy: `text`, `markdown`, `yaml`, or `semantic`. Default `text`.'
          pattern: ^(text|markdown|yaml|semantic)$
        max_characters:
          type:
            - integer
            - "null"
          format: int32
          description: Maximum characters per chunk (must be greater than `overlap`). Default 1000.
          minimum: 100
        overlap:
          type:
            - integer
            - "null"
          format: int32
          description: |-
            Overlap between adjacent chunks in characters (must be less than
            `max_characters`). Default 200.
          minimum: 0
        topic_threshold:
          type:
            - number
            - "null"
          format: float
          description: |-
            Cosine-similarity threshold (0.0–1.0) for the `semantic` chunker's topic
            boundary detection. Ignored by other chunker types.
          maximum: 1
          minimum: 0
        trim:
          type:
            - boolean
            - "null"
          description: Trim whitespace from chunk boundaries. Default true.
      additionalProperties: false
    CollectionResponse:
      type: object
      description: Response for collection retrieval / listing
      required:
        - id
        - name
        - embedding_dim
        - distance_metric
        - index_method
        - index_status
      properties:
        chunking_config:
          $ref: '#/components/schemas/CollectionChunkingConfig'
          description: Per-collection text chunking configuration
        distance_metric:
          type: string
          description: Distance metric
        embedding_dim:
          type: integer
          format: int32
          description: Embedding dimension
          minimum: 0
        embedding_source:
          type:
            - string
            - "null"
          description: Embedding source for this collection
        id:
          type: string
          format: uuid
          description: Collection unique identifier
        index_error:
          type: string
          description: Safe failure summary when `index_status` is `failed`
        index_method:
          type: string
          description: Vector index method
        index_status:
          type: string
          description: Durable state of the collection's currently served vector index.
          enum:
            - pending
            - building
            - ready
            - failed
        name:
          type: string
          description: Collection name
        quota_max_documents:
          type:
            - integer
            - "null"
          format: int64
          description: Maximum documents quota
          minimum: 0
        rerank_default:
          $ref: '#/components/schemas/RerankerSpec'
          description: Default reranker spec for retrieve operations on this collection
        stats:
          $ref: '#/components/schemas/CollectionStatsResponse'
          description: |-
            Collection statistics. Populated on the single-collection detail view
            (`GET /v1/rag/collections/{name}`); omitted from the list view, which is
            intentionally lightweight and does not run per-collection counts.
    CollectionStatsResponse:
      type: object
      description: Collection statistics response
      required:
        - document_count
        - chunk_count
      properties:
        chunk_count:
          type: integer
          format: int64
          description: Total chunks across all documents
          minimum: 0
        document_count:
          type: integer
          format: int64
          description: Total documents in collection
          minimum: 0
        last_ingested_at:
          type:
            - string
            - "null"
          description: Last ingestion timestamp (RFC3339)
    ConfirmUploadRequest:
      type: object
      description: Request body for confirming uploads
      required:
        - batch_id
      properties:
        batch_id:
          type: string
          format: uuid
          description: Batch ID from the presign response.
    ConfirmUploadResponse:
      type: object
      description: Response from confirm endpoint
      required:
        - job_ids
        - status
      properties:
        job_ids:
          type: array
          items:
            type: string
          description: Job IDs that are now queued for processing
        status:
          type: string
          description: Status of the jobs
    ContentFilterConfig:
      type: object
      description: Content filtering configuration (headers, footers, watermarks).
      properties:
        include_footers:
          type:
            - boolean
            - "null"
          description: Include running footers
        include_headers:
          type:
            - boolean
            - "null"
          description: Include running headers
        include_watermarks:
          type:
            - boolean
            - "null"
          description: Include watermarks
        strip_repeating_text:
          type:
            - boolean
            - "null"
          description: Strip cross-page repeating text. Defaults to true when omitted.
      additionalProperties: false
    ContentLayer:
      type: string
      description: |-
        Content layer classification for document nodes.

        Replaces separate body/furniture arrays with per-node granularity.
      enum:
        - body
        - header
        - footer
        - footnote
    ContributorRole:
      type: object
      description: JATS contributor with role.
      required:
        - name
      properties:
        name:
          type: string
          description: Contributor display name.
        role:
          type:
            - string
            - "null"
          description: Contributor role (e.g. `"author"`, `"editor"`).
    CrawlChildJob:
      type: object
      description: One extraction job spawned by a crawl job.
      required:
        - id
        - filename
        - status
      properties:
        body_bytes:
          type:
            - integer
            - "null"
          format: int64
          description: Source response-body size.
        filename:
          type: string
          description: Original filename.
        id:
          type: string
          format: uuid
          description: Child job identifier.
        processing_time_ms:
          type:
            - integer
            - "null"
          format: int64
          description: Processing duration in milliseconds.
        result:
          $ref: '#/components/schemas/ExtractedDocument'
          description: Embedded extraction result, when available.
        source_url:
          type:
            - string
            - "null"
          description: URL that produced the document.
        status:
          type: string
          description: Current child-job status.
    CrawlConfig:
      type: object
      description: Crawl policy configuration — controls URL crawling behavior.
      properties:
        allow_subdomains:
          type:
            - boolean
            - "null"
          description: 'Allow subdomains of the seed domain (default: true)'
          example: true
        allowed_file_types:
          type: array
          items:
            type: string
          description: 'MIME types of documents to extract (default: all supported by xberg)'
          example:
            - application/pdf
            - text/html
        browser:
          $ref: '#/components/schemas/BrowserOverrides'
          description: |-
            Browser rendering is unavailable pending connect-time DNS pinning.
            Omit this field or set `mode` to `never`; enabling modes return HTTP 400.
        bypass:
          $ref: '#/components/schemas/BypassRequestOverride'
          description: |-
            Bypass execution is unavailable pending connect-time DNS pinning.
            Any request that sets this field returns HTTP 400.
        document_url_depth:
          type:
            - integer
            - "null"
          format: int32
          description: 'Max depth for URL-following from documents (default: 1, max: 3)'
          example: 1
          maximum: 3
          minimum: 0
        download_documents:
          type:
            - boolean
            - "null"
          description: |-
            Retain downloaded document bytes on crawl pages (default: true).
            Set to false when the response does not need document content.
          example: true
        exclude_paths:
          type: array
          items:
            type: string
          description: Regex patterns — skip URLs matching these
          example:
            - /admin/.*
            - /login
        extract_page_content:
          type:
            - boolean
            - "null"
          description: 'Also extract the HTML page content as markdown (default: true)'
          example: true
        follow_document_urls:
          type:
            - boolean
            - "null"
          description: 'Follow URLs discovered inside extracted documents (default: false)'
          example: false
        include_paths:
          type: array
          items:
            type: string
          description: Regex patterns — only crawl URLs matching these
          example:
            - /docs/.*
            - /blog/.*
        max_concurrent:
          type:
            - integer
            - "null"
          format: int32
          description: 'Maximum concurrent requests (default: 10, max: 32)'
          example: 10
          maximum: 32
          minimum: 1
        max_depth:
          type:
            - integer
            - "null"
          format: int32
          description: 'Maximum crawl depth from seed URL (default: 2, max: 5)'
          example: 2
          maximum: 5
          minimum: 0
        max_pages:
          type:
            - integer
            - "null"
          format: int32
          description: 'Maximum pages to crawl (default: 1, min: 1, max: 10000)'
          example: 100
          maximum: 10000
          minimum: 1
        output_mode:
          type:
            - string
            - "null"
          description: |-
            Output mode (default: "extract").
            - "scrape":  single-page fetch, no recursion, no extraction
            - "crawl":   multi-page crawl, no extraction (raw content returned)
            - "extract": multi-page crawl + extract each discovered document
          example: extract
        proxy:
          $ref: '#/components/schemas/ProxyOverride'
          description: |-
            Proxy execution is unavailable while private-network denial is pinned.
            Any request that sets this field returns HTTP 400.
        request_timeout_secs:
          type:
            - integer
            - "null"
          format: int64
          description: 'Per-request timeout in seconds (default: 30, max: 300)'
          example: 30
          maximum: 300
          minimum: 1
        respect_robots_txt:
          type:
            - boolean
            - "null"
          description: 'Respect robots.txt (default: true)'
          example: true
        stay_on_domain:
          type:
            - boolean
            - "null"
          description: 'Stay on the same domain as seed URL (default: true)'
          example: true
        strategy:
          type:
            - string
            - "null"
          description: Crawl strategy. The crawler is breadth-first; only "bfs" is accepted.
          example: bfs
        user_agent:
          type:
            - string
            - "null"
          description: Custom User-Agent string
          example: XbergBot/1.0
      example:
        max_depth: 2
        max_pages: 100
        max_concurrent: 10
        stay_on_domain: true
        allow_subdomains: true
        respect_robots_txt: true
    CrawlEventV1:
      oneOf:
        - type: object
          description: A page was successfully crawled and indexed.
          required:
            - crawl_job_id
            - ts
            - url
            - status_code
            - depth
            - kind
          properties:
            crawl_job_id:
              type: string
              format: uuid
              description: Job ID this event belongs to
            depth:
              type: integer
              description: Crawl depth when this page was discovered
              minimum: 0
            kind:
              type: string
              enum:
                - page
            status_code:
              type: integer
              format: int32
              description: HTTP status code
              minimum: 0
            ts:
              type: string
              format: date-time
              description: Event timestamp (UTC)
            url:
              type: string
              description: URL that was crawled
        - type: object
          description: An error occurred while crawling a URL.
          required:
            - crawl_job_id
            - ts
            - url
            - error
            - kind
          properties:
            crawl_job_id:
              type: string
              format: uuid
              description: Job ID this event belongs to
            error:
              type: string
              description: Error description
            kind:
              type: string
              enum:
                - error
            terminal:
              type: boolean
              description: Whether this error is the job's terminal event.
            ts:
              type: string
              format: date-time
              description: Event timestamp (UTC)
            url:
              type: string
              description: URL that failed
        - type: object
          description: A new URL was discovered during crawling.
          required:
            - crawl_job_id
            - ts
            - url
            - depth
            - kind
          properties:
            crawl_job_id:
              type: string
              format: uuid
              description: Job ID this event belongs to
            depth:
              type: integer
              description: Depth at which this URL was discovered
              minimum: 0
            kind:
              type: string
              enum:
                - discovered
            ts:
              type: string
              format: date-time
              description: Event timestamp (UTC)
            url:
              type: string
              description: URL that was discovered
        - type: object
          description: Crawl job completed.
          required:
            - crawl_job_id
            - ts
            - pages_crawled
            - kind
          properties:
            crawl_job_id:
              type: string
              format: uuid
              description: Job ID this event belongs to
            kind:
              type: string
              enum:
                - complete
            pages_crawled:
              type: integer
              description: Total pages successfully crawled
              minimum: 0
            ts:
              type: string
              format: date-time
              description: Event timestamp (UTC)
      description: |-
        A crawl event published to the SSE stream.

        Tagged union with event kind as the discriminator. Each variant carries
        `crawl_job_id` and `ts` (timestamp) for client-side ordering and deduplication.

        Internally tagged on `kind` (`#[serde(tag = "kind")]`): each variant
        serializes as a flat JSON object with a `kind` field alongside its other
        fields, e.g. `{"kind":"page","crawl_job_id":"...","ts":"...",...}`. utoipa
        derives the matching `oneOf` schema straight from that `#[serde(tag = ..)]`
        attribute, so no separate `#[schema(...)]` tagging is needed. An explicit
        `discriminator` is not applicable here: utoipa only supports OpenAPI
        discriminators for `#[serde(untagged)]` enums with single-reference-field
        variants, not internally tagged enums like this one.
    CrawlJobResponse:
      type: object
      description: Metadata and progress for one crawl job.
      required:
        - id
        - status
        - seed_urls
        - output_mode
        - created_at
        - progress
      properties:
        created_at:
          type: string
          format: date-time
          description: ISO 8601 creation timestamp.
        id:
          type: string
          format: uuid
          description: Crawl job identifier.
        jobs:
          type: array
          items:
            $ref: '#/components/schemas/CrawlChildJob'
          description: Child extraction jobs.
        output_mode:
          type: string
          description: Requested output mode.
        pages:
          type: array
          items:
            $ref: '#/components/schemas/CrawledPage'
          description: Retained raw pages.
        progress:
          $ref: '#/components/schemas/CrawlProgress'
          description: Current progress counters.
        seed_urls:
          type: array
          items:
            type: string
          description: Submitted seed URLs.
        status:
          $ref: '#/components/schemas/CrawlJobStatus'
          description: Current crawl status.
    CrawlJobStatus:
      type: string
      description: Crawl job lifecycle status.
      enum:
        - pending
        - crawling
        - extracting
        - completed
        - partial_success
        - failed
        - cancelled
    CrawlProgress:
      type: object
      description: Progress counters for a crawl job.
      required:
        - pages_crawled
        - documents_discovered
        - documents_extracted
        - documents_failed
      properties:
        documents_discovered:
          type: integer
          format: int32
          description: Documents discovered so far.
        documents_extracted:
          type: integer
          format: int32
          description: Documents extracted successfully.
        documents_failed:
          type: integer
          format: int32
          description: Documents that failed extraction.
        pages_crawled:
          type: integer
          format: int32
          description: Pages fetched so far.
    CrawlWebhookEvent:
      type: object
      description: Crawl-level webhook body. Unlike extraction job events, it never embeds results.
      required:
        - event
        - event_id
        - crawl_job_id
        - timestamp
        - status
        - progress
      properties:
        crawl_job_id:
          type: string
          format: uuid
          description: Crawl job that reached a terminal status.
        error:
          $ref: '#/components/schemas/WebhookEventError'
          description: Error details, present only on `crawl.failed`.
        event:
          $ref: '#/components/schemas/CrawlWebhookEventType'
          description: Terminal crawl event name.
        event_id:
          type: string
          format: uuid
          description: Stable identity shared by retries of this event.
        metadata:
          type:
            - object
            - "null"
          description: Customer metadata, sorted for stable signed payload bytes.
          additionalProperties:
            type: string
          propertyNames:
            type: string
        progress:
          $ref: '#/components/schemas/CrawlProgress'
          description: The same four counters exposed by `CrawlJobResponse`.
        status:
          $ref: '#/components/schemas/CrawlWebhookStatus'
          description: The terminal crawl status, including partial success when applicable.
        timestamp:
          type: string
          format: date-time
          description: RFC 3339 time when the crawl reached a terminal status.
    CrawlWebhookEventType:
      type: string
      description: Terminal event names for crawl-level deliveries. Job event names remain separate.
      enum:
        - crawl.completed
        - crawl.failed
    CrawlWebhookStatus:
      type: string
      description: Terminal crawl status carried separately from the event name.
      enum:
        - completed
        - partial_success
        - failed
    CrawledPage:
      type: object
      description: One raw page retained by a scrape or crawl output job.
      required:
        - url
        - status
        - fetched_at
      properties:
        content_url:
          type:
            - string
            - "null"
          description: Presigned URL for the retained response body.
        error:
          type:
            - string
            - "null"
          description: Fetch error, when the page failed.
        fetched_at:
          type: string
          format: date-time
          description: ISO 8601 fetch timestamp.
        mime_type:
          type:
            - string
            - "null"
          description: Response MIME type.
        size_bytes:
          type:
            - integer
            - "null"
          format: int64
          description: Response body size.
        status:
          type: string
          description: Fetch status.
        url:
          type: string
          description: Source URL.
    CreateAutoTuneJobRequest:
      type: object
      description: Request to submit a new auto-tune job.
      required:
        - documents
      properties:
        budget:
          $ref: '#/components/schemas/AutoTuneBudget'
          description: Optional budget limits; falls back to defaults when omitted.
        documents:
          type: array
          items:
            $ref: '#/components/schemas/AutoTuneDocumentInput'
          description: Documents and their ground truth for this run. At least one is required.
          minItems: 1
        routing:
          type:
            - boolean
            - "null"
          description: |-
            Whether to tune per-document routing in addition to global
            configuration. `None`/`false` tunes global-only.
        search:
          $ref: '#/components/schemas/AutoTuneSearchConfig'
          description: Optional search configuration; falls back to defaults when omitted.
        webhook:
          $ref: '#/components/schemas/WebhookConfig'
          description: Optional terminal callback, snapshotted when the run is submitted.
    CreateAutoTuneJobResponse:
      type: object
      description: Response from submitting a new auto-tune job.
      required:
        - auto_tune_job_id
        - status
      properties:
        auto_tune_job_id:
          type: string
          description: Unique ID of the newly created auto-tune job (ULID).
        status:
          type: string
          description: Initial job status (e.g. `queued`).
    CreateCollectionRequest:
      type: object
      description: Request to create a new RAG collection.
      required:
        - name
        - embedding_dim
      properties:
        chunking_config:
          $ref: '#/components/schemas/CollectionChunkingConfig'
          description: |-
            Per-collection text chunking configuration.

            Omit any field to inherit the ingest pipeline's built-in default
            (max 1000 characters, 200-character overlap, text chunker).
        distance_metric:
          type: string
          description: Distance metric for similarity search ("cosine", "l2", "inner_product")
          pattern: ^(cosine|l2|inner_product)$
        embedding_dim:
          type: integer
          format: int32
          description: Embedding dimension (e.g., 384 for mxbai-embed-large-v1)
          maximum: 2000
          minimum: 1
        embedding_source:
          type:
            - string
            - "null"
          description: |-
            Embedding source for the collection.

            Omit to use the deployment-wide default (usually "managed_local").
            When provided, overrides the deployment default.
        index_method:
          type:
            - string
            - "null"
          description: |-
            Vector index method ("diskann" or "hnsw").

            Omit to inherit the deployment-wide default
            (`XBERG_VECTORSTORE_DEFAULT_INDEX_METHOD` env var, Helm value
            `vectorstore.defaultIndexMethod`). The API layer is responsible for
            filling the default before persisting the collection.
          pattern: ^(diskann|hnsw)$
        label_classifier_threshold:
          type:
            - number
            - "null"
          format: float
          description: |-
            Not available. Sending this field is refused with
            `rag.label_classification.unsupported`, for the same reason as
            `label_vocabulary`.
        label_vocabulary:
          type:
            - array
            - "null"
          items:
            type: string
          description: |-
            Not available. Sending this field is refused with
            `rag.label_classification.unsupported`: no build ships an ingest-time
            classifier, so a stored vocabulary would never label a document. Supply
            labels per document via `enrichment_overrides.labels` instead.
        name:
          type: string
          description: Collection name (unique per project)
          maxLength: 255
          minLength: 1
          pattern: ^[A-Za-z0-9_-]+$
        quota_max_documents:
          type:
            - integer
            - "null"
          format: int64
          description: Maximum documents quota for this collection
          minimum: 0
        rerank_default:
          $ref: '#/components/schemas/RerankerSpec'
          description: |-
            Default reranker spec for retrieve operations on this collection.

            When present, this reranker is used as the default unless the
            retrieve request overrides it. Can be a Preset (ONNX cross-encoder)
            or Llm (platform-managed LLM).
        rerank_timeout_ms:
          type:
            - integer
            - "null"
          format: int32
          description: |-
            Milliseconds a rerank may take before the primary order is returned
            instead. Omit to take the deployment default.

            The budget is per collection because a reranker's cost is a property of
            the model and the hardware, not of one request: several shipped presets
            do not finish inside the default on a CPU-only deployment. A retrieve
            that exceeds it reports `rerank.status = "fell_back"` with
            `reason = "timeout"`, so raising it is a decision the caller can make on
            evidence rather than a number they cannot see.
          maximum: 30000
          minimum: 1
      additionalProperties: false
    CreateCollectionResponse:
      type: object
      description: Response for collection creation
      required:
        - id
        - name
        - embedding_dim
        - distance_metric
        - index_method
      properties:
        chunking_config:
          $ref: '#/components/schemas/CollectionChunkingConfig'
          description: Per-collection text chunking configuration
        distance_metric:
          type: string
          description: Distance metric
        embedding_dim:
          type: integer
          format: int32
          description: Embedding dimension
          minimum: 0
        embedding_source:
          type:
            - string
            - "null"
          description: Embedding source for this collection
        id:
          type: string
          format: uuid
          description: Collection unique identifier
        index_method:
          type: string
          description: Vector index method
        name:
          type: string
          description: Collection name
        quota_max_documents:
          type:
            - integer
            - "null"
          format: int64
          description: Maximum documents quota
          minimum: 0
        rerank_default:
          $ref: '#/components/schemas/RerankerSpec'
          description: Default reranker spec for retrieve operations on this collection
    CreateDocumentAsyncResponse:
      type: object
      description: Response for asynchronous document creation
      required:
        - job_id
        - document_id
        - status
        - poll
      properties:
        document_id:
          type: string
          format: uuid
          description: The pending document ID (for reference)
        job_id:
          type: string
          description: Job identifier for polling (ULID)
        poll:
          type: string
          description: Polling endpoint
        status:
          type: string
          description: Lifecycle status
    CreateDocumentChunkInput:
      type: object
      description: Caller-supplied chunk for bring-your-own-chunks ingest
      required:
        - content
      properties:
        content:
          type: string
          description: Chunk content (text)
        embedding:
          type:
            - array
            - "null"
          items:
            type: number
            format: float
          description: |-
            Caller-supplied embedding vector. When present on every chunk in the
            request, triggers synchronous write (200). When absent on any chunk,
            the entire request goes async and the worker embeds all chunks.
        external_id:
          type:
            - string
            - "null"
          description: External identifier for this chunk (optional)
        metadata:
          type: object
          description: Metadata attached to this chunk
          additionalProperties: {}
          propertyNames:
            type: string
        ordinal:
          type:
            - integer
            - "null"
          format: int32
          description: Position in the document (0-indexed)
          minimum: 0
      additionalProperties: false
    CreateDocumentRequest:
      type: object
      description: |-
        OpenAPI schema for `CreateDocumentRequest`.

        This DTO keeps the published request schema as the flat JSON object accepted
        by the handler while runtime deserialization still uses `CreateDocumentRequest`.
      required:
        - kind
      properties:
        chunks:
          type:
            - array
            - "null"
          items:
            $ref: '#/components/schemas/CreateDocumentChunkInput'
          description: 'Pre-split chunks for `kind: "chunks"`'
        enrichment_overrides:
          $ref: '#/components/schemas/EnrichmentOverridesRequest'
          description: Pre-computed enrichments to skip the server's pass
        external_id:
          type:
            - string
            - "null"
          description: Caller-supplied external identifier for the document
        kind:
          type: string
          description: 'Source variant: `"text"` or `"chunks"`'
        metadata:
          type:
            - object
            - "null"
          description: Caller-supplied metadata stored alongside the document
          additionalProperties: {}
          propertyNames:
            type: string
        mime:
          type:
            - string
            - "null"
          description: MIME type of the source document for audit purposes
        source_object_key:
          type:
            - string
            - "null"
          description: GCS/S3 object key for the source document
        text:
          type:
            - string
            - "null"
          description: 'Raw text content for `kind: "text"`'
        title:
          type:
            - string
            - "null"
          description: Human-readable document title
        webhook:
          $ref: '#/components/schemas/WebhookConfig'
          description: Optional webhook for ingest completion/failure delivery
    CreateDocumentResponse:
      type: object
      description: Response for synchronous document creation (BYO-vector fast path)
      required:
        - document_id
        - collection
        - chunk_count
        - embedding_dim
        - ingested_at
      properties:
        chunk_count:
          type: integer
          format: int32
          description: Number of chunks created
        collection:
          type: string
          description: Collection name
        document_id:
          type: string
          format: uuid
          description: Unique document identifier
        embedding_dim:
          type: integer
          format: int32
          description: Embedding dimension (from collection spec)
        external_id:
          type:
            - string
            - "null"
          description: Caller-supplied external ID (if provided)
        ingested_at:
          type: string
          format: date-time
          description: Ingestion timestamp (RFC3339)
    CreateDocumentSource:
      oneOf:
        - $ref: '#/components/schemas/CreateDocumentSourceText'
        - $ref: '#/components/schemas/CreateDocumentSourceChunks'
      description: Document source variant for ingest
      discriminator:
        propertyName: kind
        mapping:
          chunks: '#/components/schemas/CreateDocumentSourceChunks'
          text: '#/components/schemas/CreateDocumentSourceText'
    CreateDocumentSourceChunks:
      type: object
      description: Pre-chunked document — chunks may or may not have embeddings
      required:
        - chunks
        - kind
      properties:
        chunks:
          type: array
          items:
            $ref: '#/components/schemas/CreateDocumentChunkInput'
          description: List of pre-split chunks
        enrichment_overrides:
          $ref: '#/components/schemas/EnrichmentOverridesRequest'
          description: Pre-computed enrichments to skip server's pass
        kind:
          type: string
          enum:
            - chunks
    CreateDocumentSourceText:
      type: object
      description: Full-text document — server runs chunking + enrichment + embedding (async)
      required:
        - text
        - kind
      properties:
        enrichment_overrides:
          $ref: '#/components/schemas/EnrichmentOverridesRequest'
          description: Pre-computed enrichments (keywords, entities, labels) to skip server's pass
        kind:
          type: string
          enum:
            - text
        text:
          type: string
          description: Raw text content
    CreateSavedPresetRequest:
      type: object
      description: Request to create a saved preset.
      required:
        - name
        - schema
        - preferred_call_mode
      properties:
        context_template:
          type:
            - string
            - "null"
          description: Optional context template.
        description:
          type:
            - string
            - "null"
          description: Optional description of the preset.
        emit_citations:
          type: boolean
          description: |-
            Whether to emit citations in results.

            Stored and returned verbatim, but **not applied at extraction time** —
            see `preferred_call_mode`. Results produced from a saved preset carry no
            citations regardless of this value. Defaults to `true` when omitted.
        name:
          type: string
          description: |-
            Preset name (must be unique per project). Trimmed before storage; a
            name that is entirely whitespace is rejected once trimmed.
          maxLength: 255
          minLength: 1
          pattern: \S
        preferred_call_mode:
          type: string
          description: |-
            Preferred orchestration path (`text_only`, `vision_only`, or
            `text_plus_vision`).

            Validated on write, stored, and returned verbatim, but
            **not applied at extraction time**. Call-mode selection belongs to the
            vision pipeline, which a job reaches only through the curated preset
            registry (`options.preset`); a saved preset never routes there. On Xberg
            Pro a saved preset contributes its `schema`, `system_prompt` and
            `context_template`, and nothing else.
          pattern: ^(text_only|vision_only|text_plus_vision)$
        schema:
          type: object
          description: JSON Schema (Draft 2020-12) for the extraction result.
        system_prompt:
          type:
            - string
            - "null"
          description: Optional custom system prompt override.
    CreateSavedPresetResponse:
      type: object
      description: Response from creating a saved preset.
      required:
        - success
        - id
        - name
        - message
      properties:
        id:
          type: string
          format: uuid
          description: The newly created preset's id.
        message:
          type: string
          description: Human-readable confirmation message.
        name:
          type: string
          description: The preset's name (same value as the request).
        success:
          type: boolean
          description: Always `true` on success (the endpoint returns an error status otherwise).
    CsvMetadata:
      type: object
      description: CSV/TSV file metadata.
      required:
        - row_count
        - column_count
        - has_header
        - format_type
      properties:
        column_count:
          type: integer
          format: int32
          description: Number of columns detected.
          minimum: 0
        column_types:
          type:
            - array
            - "null"
          items:
            type: string
          description: Inferred data type for each column (e.g. `"string"`, `"integer"`, `"float"`).
        delimiter:
          type:
            - string
            - "null"
          description: Field delimiter character (e.g. `","` or `"\t"`).
        format_type:
          type: string
          enum:
            - csv
        has_header:
          type: boolean
          description: Whether the first row was treated as a header.
        row_count:
          type: integer
          format: int32
          description: Total number of data rows (excluding the header row if present).
          minimum: 0
    DbfFieldInfo:
      type: object
      description: dBASE field information.
      required:
        - name
        - field_type
      properties:
        field_type:
          type: string
          description: dBASE field type character (e.g. `"C"` for character, `"N"` for numeric).
        name:
          type: string
          description: Field (column) name.
    DbfMetadata:
      type: object
      description: dBASE (DBF) file metadata.
      required:
        - record_count
        - field_count
        - format_type
      properties:
        field_count:
          type: integer
          description: Number of field (column) definitions.
          minimum: 0
        fields:
          type: array
          items:
            $ref: '#/components/schemas/DbfFieldInfo'
          description: Descriptor for each field in the table schema.
        format_type:
          type: string
          enum:
            - dbf
        record_count:
          type: integer
          description: Total number of data records in the DBF file.
          minimum: 0
    DeleteDocumentsRequest:
      type: object
      description: OpenAPI schema for `DeleteDocumentsRequest`.
      properties:
        filter:
          type: object
          description: Vectorstore filter expression.
        ids:
          type:
            - array
            - "null"
          items:
            type: string
            format: uuid
          description: List of document UUIDs to delete.
    DeleteDocumentsResponse:
      type: object
      description: Response for deletion operation
      required:
        - deleted_count
      properties:
        deleted_count:
          type: integer
          format: int64
          description: Number of documents deleted
          minimum: 0
    DiffAsyncAccepted:
      type: object
      description: Response for a submitted async diff job.
      required:
        - diff_job_id
        - status
      properties:
        diff_job_id:
          type: string
          format: uuid
          description: The ID of the diff job for polling.
        status:
          $ref: '#/components/schemas/DiffJobStatus'
          description: Current status of the job.
    DiffJobStatus:
      type: string
      description: Status of an async diff job.
      enum:
        - pending
        - succeeded
        - failed
    DiffLine:
      oneOf:
        - type: object
          description: Unchanged context line.
          required:
            - text
            - kind
          properties:
            kind:
              type: string
              enum:
                - context
            text:
              type: string
              description: Unchanged context line.
        - type: object
          description: Line added in the "after" version.
          required:
            - text
            - kind
          properties:
            kind:
              type: string
              enum:
                - added
            text:
              type: string
              description: Line added in the "after" version.
        - type: object
          description: Line removed from the "before" version.
          required:
            - text
            - kind
          properties:
            kind:
              type: string
              enum:
                - removed
            text:
              type: string
              description: Line removed from the "before" version.
      description: |-
        A single line in a unified-diff hunk.

        Defined here (rather than only in `crate::diff`) so `RevisionDelta` can
        reference it unconditionally, without requiring the `diff` Cargo feature.
        `crate::diff` re-exports this type verbatim.
    DiffQuery:
      type: object
      description: Query parameters for diff computation.
      required:
        - from
        - to
      properties:
        from:
          oneOf:
            - type: integer
              format: int32
              maximum: 2147483647
              minimum: -2147483648
            - type: string
              format: uuid
        to:
          oneOf:
            - type: integer
              format: int32
              maximum: 2147483647
              minimum: -2147483648
            - type: string
              format: uuid
    DiffResponse:
      type: object
      description: Response envelope for a computed diff between two extraction results.
      required:
        - from_job_id
        - to_job_id
        - from_version
        - to_version
        - document_id
        - diff
        - computed_at
      properties:
        computed_at:
          type: string
          format: date-time
          description: Timestamp when the diff was computed.
        diff:
          type: object
          description: |-
            The computed diff between the two extraction results.

            Serialized to the wire as the full `xberg::diff::ExtractionDiff`
            shape. The OpenAPI schema declares this as an opaque object because
            the underlying type recurses through `EmbeddedDiff -> Box<ExtractionDiff>`
            and utoipa's schema collector goes into a stack-overflow on inlined
            recursion. SDKs that want a typed surface here should decode the
            JSON manually against xberg's published schema.
        document_id:
          type: string
          format: uuid
          description: Document ID being diffed.
        from_job_id:
          type: string
          format: uuid
          description: Job ID of the "from" (older) version.
        from_version:
          type: integer
          format: int32
          description: Version sequence number of the "from" version.
        to_job_id:
          type: string
          format: uuid
          description: Job ID of the "to" (newer) version.
        to_version:
          type: integer
          format: int32
          description: Version sequence number of the "to" version.
    DjotAttributeGroup:
      type: object
      required:
        - identifier
        - attributes
      properties:
        attributes:
          type: object
          description: |-
            Element attributes in Djot.

            Represents the attributes attached to elements using {.class #id key="value"} syntax.
          properties:
            classes:
              type: array
              items:
                type: string
              description: CSS classes (.class1 .class2)
            id:
              type:
                - string
                - "null"
              description: Element ID (#identifier)
            key_values:
              type: array
              items:
                $ref: '#/components/schemas/KeyValueAttribute'
              description: Key-value pairs (key="value")
        identifier:
          type: string
    DjotContent:
      type: object
      description: |-
        Comprehensive Djot document structure with semantic preservation.

        This type captures the full richness of Djot markup, including:
        - Block-level structures (headings, lists, blockquotes, code blocks, etc.)
        - Inline formatting (emphasis, strong, highlight, subscript, superscript, etc.)
        - Attributes (classes, IDs, key-value pairs)
        - Links, images, footnotes
        - Math expressions (inline and display)
        - Tables with full structure

        Available when the `djot` feature is enabled.
      required:
        - plain_text
        - blocks
        - metadata
        - tables
        - images
        - links
        - footnotes
      properties:
        attributes:
          type: array
          items:
            $ref: '#/components/schemas/DjotAttributeGroup'
          description: Attributes mapped by element identifier (if present)
        blocks:
          type: array
          items:
            $ref: '#/components/schemas/FormattedBlock'
          description: Structured block-level content
        footnotes:
          type: array
          items:
            $ref: '#/components/schemas/Footnote'
          description: Footnote definitions
        images:
          type: array
          items:
            $ref: '#/components/schemas/DjotImage'
          description: Extracted images with metadata
        links:
          type: array
          items:
            $ref: '#/components/schemas/DjotLink'
          description: Extracted links with URLs
        metadata:
          $ref: '#/components/schemas/Metadata'
          description: Metadata from YAML frontmatter
        plain_text:
          type: string
          description: Plain text representation for backwards compatibility
        tables:
          type: array
          items:
            $ref: '#/components/schemas/Table'
          description: Extracted tables as structured data
    DjotImage:
      type: object
      description: Image element in Djot.
      required:
        - src
        - alt
      properties:
        alt:
          type: string
          description: Alternative text
        attributes:
          $ref: '#/components/schemas/Attributes'
          description: Element attributes
        src:
          type: string
          description: Image source URL or path
        title:
          type:
            - string
            - "null"
          description: Optional title
    DjotLink:
      type: object
      description: Link element in Djot.
      required:
        - url
        - text
      properties:
        attributes:
          $ref: '#/components/schemas/Attributes'
          description: Element attributes
        text:
          type: string
          description: Link text content
        title:
          type:
            - string
            - "null"
          description: Optional title
        url:
          type: string
          description: Link URL
    DocumentCounts:
      type: object
      description: |-
        Cheap structural counts for an extracted document.

        Populated on every `ExtractedDocument` returned by `extract` /
        `extract_batch`, regardless of whether the heavy `pages` / `images`
        collections are materialized. A caller that only needs "how many pages /
        tables / images did this document have?" (reporting, cost estimation,
        progress, quotas) can read these without enabling per-page or per-image
        extraction.

        The page count comes from the parse (the extractor already walks the page
        tree); it does not require opting into per-page content. `pages` is `0` for
        inputs that are not page-addressable (e.g. plain text).
      required:
        - pages
        - tables
        - images
      properties:
        images:
          type: integer
          description: Images detected in the document.
          minimum: 0
        pages:
          type: integer
          description: Total pages in the source document (`0` when not page-addressable).
          minimum: 0
        tables:
          type: integer
          description: Tables detected in the document.
          minimum: 0
    DocumentInput:
      type: object
      description: |-
        A single document to extract.

        A document is sourced **either** inline (`data`, base64-encoded) **or** by
        reference to a connected integration (`integration`). Exactly one of the
        two must be present; supplying both or neither is a validation error.

        A single-document extraction is simply a `documents` vector of length one
        on `ExtractRequest`; both tiers also accept a batch of up to
        `MAX_DOCUMENTS_PER_REQUEST` entries (10 on both Pro and Enterprise).
      required:
        - filename
        - mime_type
      properties:
        config:
          $ref: '#/components/schemas/FileExtractionConfig'
          description: |-
            Per-file extraction config override.

            Precedence, highest to lowest: this override > the request's
            batch-level `options.extraction_config` > a curated/saved preset > the
            project default. The merge is shallow: a top-level field this override
            sets replaces the batch-level value outright, and a field it leaves
            unset falls through to whatever the batch level (already resolved
            against any preset and project default) carries for it. Omitting
            `config` entirely means "use the batch default" for every field. On a
            multipart request the equivalent is a `config-<filename>` JSON text
            part, mirroring the existing `document_id-<filename>` convention.
        data:
          type:
            - string
            - "null"
          description: Base64-encoded document bytes. Mutually exclusive with `integration`.
        document_id:
          type:
            - string
            - "null"
          description: |-
            Optional client-supplied document identifier. When provided, this
            extraction is recorded as a version of that logical document and the
            response carries `document_id` + `version_sequence` on the matching
            `DocumentSubmission`. Enterprise-only lineage feature; Pro leaves
            this unset. Server mints a new identifier when absent. Typed as a string
            to span Pro (ULID) and Enterprise (UUID) identifier forms; the concrete
            format is validated per service.
        filename:
          type: string
          description: Original filename.
          maxLength: 255
          minLength: 1
        integration:
          $ref: '#/components/schemas/IntegrationInput'
          description: |-
            Reference to a document in a connected integration. Mutually exclusive
            with `data`.
        mime_type:
          type: string
          description: |-
            MIME type of the document. For an integration-sourced document this is
            a hint only — the receiving service prefers the connector-reported
            type at fetch time.
      additionalProperties: false
    DocumentNode:
      type: object
      description: |-
        A single node in the document tree.

        Each node has deterministic `id`, typed `content`, optional `parent`/`children`
        for tree structure, and metadata like page number, bounding box, and content layer.
      required:
        - content
      properties:
        annotations:
          type: array
          items:
            $ref: '#/components/schemas/TextAnnotation'
          description: |-
            Inline annotations (formatting, links) on this node's text content.

            Only meaningful for text-carrying nodes; empty for containers.
        attributes:
          type:
            - object
            - "null"
          description: |-
            Format-specific key-value attributes.

            Extensible bag for miscellaneous data without a dedicated typed field: CSS classes,
            LaTeX environment names, Excel cell formulas, slide layout names, etc.
          additionalProperties:
            type: string
          propertyNames:
            type: string
        bbox:
          $ref: '#/components/schemas/BoundingBox'
          description: Bounding box in document coordinates.
        children:
          type: array
          items:
            $ref: '#/components/schemas/NodeIndex'
          description: Child node indices in reading order.
        content:
          $ref: '#/components/schemas/NodeContent'
          description: Node content — tagged enum, type-specific data only.
        content_layer:
          $ref: '#/components/schemas/ContentLayer'
          description: |-
            Content layer classification.

            Always serialised — Kotlin-Android (and any other typed binding) treats
            the field as non-nullable, so omitting it from the JSON wire would
            break consumer deserialisation.  `#[serde(default)]` covers the
            missing-field case on inbound JSON.
        id:
          type: string
          description: |-
            Deterministic identifier (hash of node type + text + page + position).

            Stable and unique within a single extraction response: every internal
            construction path threads the node's position (its index in
            `DocumentStructure::nodes`) into the hash, so identical
            `(node_type, text, page)` tuples at different positions never collide.
            Always serialised — `ChunkMetadata::node_ids` references it to join
            chunks back to the nodes they were derived from.
            `#[serde(default)]` covers the missing-field case on inbound JSON
            (e.g. documents serialised before this field existed).
        page:
          type:
            - integer
            - "null"
          format: int32
          description: Page number where this node starts (1-indexed).
          minimum: 0
        page_end:
          type:
            - integer
            - "null"
          format: int32
          description: Page number where this node ends (for multi-page tables/sections).
          minimum: 0
        parent:
          $ref: '#/components/schemas/NodeIndex'
          description: Parent node index (`None` = root-level node).
    DocumentRelationship:
      type: object
      description: A resolved relationship between two nodes in the document tree.
      required:
        - source
        - target
        - kind
      properties:
        kind:
          $ref: '#/components/schemas/RelationshipKind'
          description: Semantic kind of the relationship.
        source:
          $ref: '#/components/schemas/NodeIndex'
          description: Source node index (the referencing node).
        target:
          $ref: '#/components/schemas/NodeIndex'
          description: Target node index (the referenced node).
    DocumentRevision:
      type: object
      description: |-
        A single tracked change embedded in a document.

        Populated by per-format extractors that understand change-tracking metadata
        (DOCX `w:ins`/`w:del`/`w:rPrChange`, ODT `text:change-*`, …). Every
        extractor defaults to `ExtractedDocument.revisions = None` until a
        format-specific implementation is added.
      required:
        - revision_id
        - kind
        - delta
      properties:
        anchor:
          oneOf:
            - $ref: '#/components/schemas/RevisionAnchor'
              description: |-
                Best-effort document location for this revision.

                Resolution is format-dependent and may be `None` when the location
                cannot be determined (e.g. changes inside table cells before
                table-cell anchor support is added).
            - type: "null"
        author:
          type:
            - string
            - "null"
          description: Display name of the author who made this change, when available.
        delta:
          $ref: '#/components/schemas/RevisionDelta'
          description: The content changes that make up this revision.
        kind:
          $ref: '#/components/schemas/RevisionKind'
          description: Semantic kind of this revision.
        revision_id:
          type: string
          description: |-
            Format-specific revision identifier.

            For DOCX this is the `w:id` attribute value on the change element
            (e.g. `"42"`). When the attribute is absent a synthetic fallback is
            generated (`"docx-ins-0"`, `"docx-del-3"`, …).
        timestamp:
          type:
            - string
            - "null"
          description: |-
            ISO-8601 timestamp of the change, when available.

            Stored as a plain string so this type remains FFI-friendly and
            unconditionally available without the `chrono` optional dep.
            DOCX populates this from the `w:date` attribute (e.g.
            `"2024-03-15T10:30:00Z"`).
    DocumentStructure:
      type: object
      description: |-
        Top-level structured document representation.

        A flat array of nodes with index-based parent/child references forming a tree.
        Root-level nodes have `parent: None`. Use `body_roots()` and `furniture_roots()`
        to iterate over top-level content by layer.

        # Validation

        Call `validate()` after construction to verify all node indices are in bounds
        and parent-child relationships are bidirectionally consistent.
      required:
        - nodes
      properties:
        node_types:
          type: array
          items:
            type: string
          description: |-
            Sorted, deduplicated list of node type names present in this document.

            Each value is the snake_case `node_type` tag of the corresponding
            `NodeContent` variant (e.g. `"paragraph"`, `"heading"`, `"table"`, …).

            Computed from `nodes` via node-type derivation.
            Empty until that method is called (internal construction paths call it
            at the end of derivation).
        nodes:
          type: array
          items:
            $ref: '#/components/schemas/DocumentNode'
          description: All nodes in document/reading order.
        relationships:
          type: array
          items:
            $ref: '#/components/schemas/DocumentRelationship'
          description: |-
            Resolved relationships between nodes (footnote refs, citations, anchor links, etc.).

            Populated during derivation from the internal document representation.
            Empty when no relationships are detected.
        source_format:
          type:
            - string
            - "null"
          description: |-
            Origin format identifier (e.g. "docx", "pptx", "html", "pdf").

            Allows renderers to apply format-aware heuristics when converting
            the document tree to output formats.
    DocumentSubmission:
      type: object
      description: Per-document submission detail included in `ExtractResponse.documents`.
      required:
        - job_id
      properties:
        document_id:
          type:
            - string
            - "null"
          description: |-
            Document lineage identifier, when the submission set `document_id`
            or one was minted server-side. `None` for non-lineage jobs.
        job_id:
          type: string
          format: uuid
          description: Job ID created for this document.
        version_sequence:
          type:
            - integer
            - "null"
          format: int32
          description: |-
            Version sequence number within the document's lineage, when lineage
            tracking applies.
    DocumentSummary:
      type: object
      description: Summary of an extracted document.
      required:
        - text
        - strategy
      properties:
        strategy:
          $ref: '#/components/schemas/SummaryStrategy'
          description: Strategy that produced this summary.
        text:
          type: string
          description: Summary text (plain prose).
        token_count:
          type:
            - integer
            - "null"
          format: int32
          description: Approximate token count of the summary, when known.
          minimum: 0
    DocumentVersionEntry:
      type: object
      description: Entry in the document versions list.
      required:
        - job_id
        - version_sequence
        - content_hash
        - status
        - filename
        - mime_type
        - file_size_bytes
        - pages_extracted
        - billable_pages
        - created_at
      properties:
        billable_pages:
          type: integer
          format: int32
          description: Number of billable pages.
        content_hash:
          type:
            - string
            - "null"
          description: |-
            SHA-256 hash of the original uploaded content, encoded as 64 lowercase hexadecimal characters.
            Legacy versions without a stored hash return `null`.
          maxLength: 64
          minLength: 64
          pattern: ^[0-9a-f]{64}$
        created_at:
          type: string
          format: date-time
          description: When the job was created.
        file_size_bytes:
          type: integer
          format: int64
          description: File size in bytes.
        filename:
          type: string
          description: Original filename.
        job_id:
          type: string
          format: uuid
          description: Job ID for this version.
          example: 550e8400-e29b-41d4-a716-446655440000
        mime_type:
          type: string
          description: MIME type of the uploaded file.
        pages_extracted:
          type: integer
          format: int32
          description: Number of pages extracted.
        processing_completed_at:
          type:
            - string
            - "null"
          description: When processing completed (only set for completed/failed jobs).
        status:
          type: string
          description: Job status.
        version_sequence:
          type: integer
          format: int32
          description: Version sequence number (incrementing per document).
    DocxMetadata:
      type: object
      description: |-
        Word document metadata.

        Extracted from DOCX files using shared Office Open XML metadata extraction.
        Integrates with `office_metadata` module for core/app/custom properties.
      required:
        - format_type
      properties:
        app_properties:
          type:
            - object
            - "null"
          description: |-
            Application properties from docProps/app.xml (Word-specific statistics)

            Contains word count, page count, paragraph count, editing time, etc.
            DOCX-specific variant of Office application properties.
        core_properties:
          type:
            - object
            - "null"
          description: |-
            Core properties from docProps/core.xml (Dublin Core metadata)

            Contains title, creator, subject, keywords, dates, etc.
            Shared format across DOCX/PPTX/XLSX documents.
        custom_properties:
          type:
            - object
            - "null"
          description: |-
            Custom properties from docProps/custom.xml (user-defined properties)

            Contains key-value pairs defined by users or applications.
            Values can be strings, numbers, booleans, or dates.
          additionalProperties: {}
          propertyNames:
            type: string
        format_type:
          type: string
          enum:
            - docx
    Element:
      type: object
      description: |-
        Semantic element extracted from document.

        Represents a logical unit of content with semantic classification,
        unique identifier, and metadata for tracking origin and position.
      required:
        - element_type
        - text
        - metadata
      properties:
        element_id:
          type: string
          description: |-
            Deterministic element identifier. Empty only when deserializing legacy payloads
            that predate this field's wire representation.
        element_type:
          $ref: '#/components/schemas/ElementType'
          description: Semantic type of this element
        metadata:
          $ref: '#/components/schemas/ElementMetadata'
          description: Metadata about the element
        text:
          type: string
          description: Text content of the element
    ElementMetadata:
      type: object
      description: Metadata for a semantic element.
      required:
        - additional
      properties:
        additional:
          type: object
          description: Additional custom metadata
          additionalProperties:
            type: string
          propertyNames:
            type: string
        coordinates:
          oneOf:
            - $ref: '#/components/schemas/BoundingBox'
              description: Bounding box coordinates if available
            - type: "null"
        element_index:
          type:
            - integer
            - "null"
          description: Position index in the element sequence
          minimum: 0
        filename:
          type:
            - string
            - "null"
          description: Source filename or document name
        page_number:
          type:
            - integer
            - "null"
          format: int32
          description: Page number (1-indexed)
          minimum: 0
    ElementType:
      type: string
      description: |-
        Semantic element type classification.

        Categorizes text content into semantic units for downstream processing.
        Supports the element types commonly found in Unstructured documents.
      enum:
        - title
        - narrative_text
        - heading
        - list_item
        - table
        - image
        - page_break
        - code_block
        - formula
        - block_quote
        - footer
        - header
    EmailMetadata:
      type: object
      description: |-
        Email metadata extracted from .eml and .msg files.

        Includes sender/recipient information, message ID, and attachment list.
      required:
        - to_emails
        - cc_emails
        - bcc_emails
        - attachments
        - format_type
      properties:
        attachments:
          type: array
          items:
            type: string
          description: List of attachment filenames
        bcc_emails:
          type: array
          items:
            type: string
          description: BCC recipients
        cc_emails:
          type: array
          items:
            type: string
          description: CC recipients
        format_type:
          type: string
          enum:
            - email
        from_email:
          type:
            - string
            - "null"
          description: Sender's email address
        from_name:
          type:
            - string
            - "null"
          description: Sender's display name
        message_id:
          type:
            - string
            - "null"
          description: Message-ID header value
        to_emails:
          type: array
          items:
            type: string
          description: Primary recipients
    EmbeddingConfig:
      type: object
      description: Embedding generation configuration for chunks.
      properties:
        batch_size:
          type:
            - integer
            - "null"
          format: int32
          description: Batch size for embedding generation. Defaults to 32 when omitted.
          minimum: 0
        model:
          type: object
          description: 'Model configuration (flexible JSON: {"type":"preset","name":"balanced"})'
        normalize:
          type:
            - boolean
            - "null"
          description: Normalize embedding vectors. Defaults to true when omitted.
        show_download_progress:
          type:
            - boolean
            - "null"
          description: Show model download progress
      additionalProperties: false
    EnrichCaptioningOptions:
      type: object
      title: EnrichCaptioningOptions
      description: 'Captioning options: caller-supplied images plus the VLM used to caption them.'
      required:
        - images
        - llm
      properties:
        images:
          type: array
          items:
            type: string
          description: |-
            Base64-encoded image bytes, in order. Each entry gets its own parallel
            slot in `EnrichResult::captions`.
        llm:
          $ref: '#/components/schemas/EnrichLlmConfig'
          description: VLM used for captioning.
      additionalProperties: false
    EnrichChunkClassificationDefinition:
      type: object
      title: EnrichChunkClassificationDefinition
      description: |-
        One label definition for chunk classification: a name plus a description of
        when it applies, injected verbatim into the classification prompt.
      required:
        - label
        - description
      properties:
        description:
          type: string
          description: Semantic description of when this label applies.
        label:
          type: string
          description: Label name returned in the matching entry of `chunk_classifications`.
      additionalProperties: false
    EnrichChunkClassificationOptions:
      type: object
      title: EnrichChunkClassificationOptions
      description: |-
        Chunk-classification options. The caller supplies its own chunk texts:
        `/v1/enrich` operates on a single text blob with no chunking pipeline of its
        own, unlike the file-extraction pipeline's `ExtractedDocument::chunks`.
      required:
        - chunks
        - definitions
        - llm
      properties:
        chunks:
          type: array
          items:
            type: string
          description: |-
            Chunk texts to classify, in order. Each entry gets its own parallel slot
            in `EnrichResult::chunk_classifications`.
        definitions:
          type: array
          items:
            $ref: '#/components/schemas/EnrichChunkClassificationDefinition'
          description: |-
            The set of label definitions the classifier may emit. Must contain at
            least one entry.
        llm:
          $ref: '#/components/schemas/EnrichLlmConfig'
          description: LLM used for classification.
      additionalProperties: false
    EnrichClassificationOptions:
      type: object
      title: EnrichClassificationOptions
      description: Whole-text classification options.
      required:
        - labels
        - llm
      properties:
        labels:
          type: array
          items:
            type: string
          description: The set of labels the classifier may emit. Must contain at least one entry.
        llm:
          $ref: '#/components/schemas/EnrichLlmConfig'
          description: LLM used for classification.
        multi_label:
          type: boolean
          description: Allow multiple labels for the text. Single-label mode returns at most one label.
      additionalProperties: false
    EnrichJobStatus:
      oneOf:
        - type: object
          description: Job submitted, awaiting processing.
          required:
            - status
          properties:
            status:
              type: string
              enum:
                - pending
        - type: object
          description: Processing completed successfully.
          required:
            - result
            - status
          properties:
            result:
              $ref: '#/components/schemas/EnrichResult'
              description: Enrichment result.
            status:
              type: string
              enum:
                - completed
        - type: object
          description: Processing failed.
          required:
            - error
            - status
          properties:
            error:
              type: string
              description: Error message.
            status:
              type: string
              enum:
                - failed
      description: |-
        Status response for an enrichment job.

        `Completed` is far larger than the other two variants now that
        `EnrichResult` carries classification, chunk classifications and captions
        (#1568). Boxing it would satisfy the lint but is the wrong trade here: the
        wire shape is fixed by the API, and `Box<EnrichResult>` would churn every
        construction site on both tiers to save a stack copy. Pro's per-job status
        cache (`services/pro/src/jobs/enrich.rs`) does hold these in a collection
        now, but weighs entries by encoded size rather than by count, so the large
        variant costs proportionally more of the cache's budget instead of being
        undercounted as "one entry" the way boxing-for-a-collection would invite.
    EnrichJobSubmitted:
      type: object
      title: EnrichJobSubmitted
      description: Response from submitting an enrichment job.
      required:
        - job_id
      properties:
        job_id:
          type: string
          description: Unique job identifier (ULID).
    EnrichLlmConfig:
      type: object
      title: EnrichLlmConfig
      description: |-
        LLM/VLM provider configuration for an enrichment stage, routed through
        liter-llm. A narrower, enrichment-scoped mirror of
        `LlmConfig`'s wire shape.

        `Debug` is implemented by hand: unlike `LlmConfig`, which prints `base_url`'s
        *presence* through `Option`'s derived `Debug`, this type prints the same
        `"<redacted>"` marker whether `base_url` is set or not. Whether a caller
        routed a request at its own endpoint is, on its own, information about that
        caller's infrastructure — not only the endpoint value itself (#1864 residual
        tracked for the Pro enrichment provider paths in `services/pro/src/jobs/enrich_provider.rs`).
      required:
        - model
      properties:
        api_key:
          type:
            - string
            - "null"
          description: |-
            API key for the provider. When omitted, liter-llm falls back to the
            provider's standard environment variable on this deployment. Treated as
            a secret — never logged.
        base_url:
          type:
            - string
            - "null"
          description: |-
            Override the provider base URL. Setting this never falls back to this
            deployment's own provider credentials — see
            `services/pro/src/jobs/enrich_provider.rs::to_xberg_llm_config`.
        max_retries:
          type:
            - integer
            - "null"
          format: int32
          description: Max retry attempts. Clamped to this deployment's ceiling.
          minimum: 0
        max_tokens:
          type:
            - integer
            - "null"
          format: int64
          description: Maximum tokens to generate.
          minimum: 0
        model:
          type: string
          description: liter-llm routing identifier, e.g. "openai/gpt-4o-mini".
        temperature:
          type:
            - number
            - "null"
          format: double
          description: Sampling temperature.
        timeout_secs:
          type:
            - integer
            - "null"
          format: int64
          description: Request timeout in seconds. Clamped to this deployment's ceiling.
          minimum: 0
      additionalProperties: false
    EnrichRequestOptions:
      type: object
      title: EnrichRequestOptions
      description: |-
        Enrichment processing options.

        Unknown keys are refused: an option this type does not carry is one the
        caller asked for and will not get, and a 202 would report otherwise.
      properties:
        captioning:
          $ref: '#/components/schemas/EnrichCaptioningOptions'
          description: |-
            VLM captioning of caller-supplied images. Enterprise refuses a
            non-`None` value; only Xberg Pro implements it (#1568).
        chunk_classification:
          $ref: '#/components/schemas/EnrichChunkClassificationOptions'
          description: |-
            Multi-label classification of caller-supplied chunks against a described
            label taxonomy. `/v1/enrich` has no chunking pipeline of its own, so the
            caller supplies the chunk texts directly. Enterprise refuses a
            non-`None` value; only Xberg Pro implements it (#1568).
        classification:
          $ref: '#/components/schemas/EnrichClassificationOptions'
          description: |-
            Whole-text zero-shot classification against a caller-supplied label set.
            Enterprise refuses a non-`None` value; only Xberg Pro implements it (#1568).
        entities:
          type: boolean
          description: Extract named entities via NER.
        keywords:
          type: boolean
          description: Extract keywords from the text.
        labels:
          type: array
          items:
            type: string
          description: |-
            Candidate labels for classification.

            Xberg Pro refuses non-empty values with
            `rag.label_classification.unsupported` because it ships no classifier.
      additionalProperties: false
    EnrichResult:
      type: object
      title: EnrichResult
      description: Result of enrichment processing.
      properties:
        captions:
          type: array
          items:
            type: string
          description: |-
            Per-image captions, positionally parallel to the submitted
            `captioning.images`. Empty when captioning was not requested.
        chunk_classifications:
          type: array
          items:
            type: array
            items:
              $ref: '#/components/schemas/ClassificationLabel'
          description: |-
            Per-chunk classification labels, positionally parallel to the submitted
            `chunk_classification.chunks`. Empty when chunk classification was not
            requested.
        classification:
          type:
            - array
            - "null"
          items:
            $ref: '#/components/schemas/ClassificationLabel'
          description: |-
            Whole-text classification labels, or `None` when classification was not
            requested. `Some(vec![])` means it ran and matched no label.
        entities:
          type:
            - array
            - "null"
          items:
            type: object
          description: Extracted named entities, or null when entity detection was not requested.
        keywords:
          type: array
          items:
            type: string
          description: Extracted keywords.
        labels:
          description: Labels selected by a classifier, or null when classification was not requested or available.
    EnrichTextRequest:
      type: object
      title: EnrichTextRequest
      description: Request to submit text for enrichment.
      required:
        - text
      properties:
        options:
          $ref: '#/components/schemas/EnrichRequestOptions'
          description: Enrichment options.
        text:
          type: string
          description: Text to enrich (max 1 MB).
    EnrichmentOverridesRequest:
      type: object
      description: |-
        Request shape of `enrichment_overrides`.

        The NATS message type stays lenient so an older publisher's payload still
        decodes, but the request boundary is strict: an unknown key is a typo the
        caller must see, and `entities` and `labels` carry typed shapes so a
        malformed value fails at parse rather than reaching the pipeline as an
        opaque blob that no later step can reject.
      properties:
        entities:
          type:
            - array
            - "null"
          items:
            type: object
            additionalProperties: {}
            propertyNames:
              type: string
          description: Pre-computed entities, as a list of objects.
        keywords:
          type:
            - array
            - "null"
          items:
            type: string
          description: Pre-computed keywords.
        labels:
          type:
            - array
            - "null"
          items:
            type: string
          description: Pre-computed labels, as a list of strings.
      additionalProperties: false
    Entity:
      type: object
      description: A single named entity detected in the extracted text.
      required:
        - category
        - text
        - start
        - end
      properties:
        category:
          $ref: '#/components/schemas/EntityCategory'
          description: Canonical category the entity belongs to (PERSON, ORG, LOCATION, etc.).
        confidence:
          type:
            - number
            - "null"
          format: float
          description: |-
            Backend-reported confidence in `[0.0, 1.0]`. `None` when the backend does not
            expose confidence scores.
        end:
          type: integer
          format: int32
          description: Byte-offset span in `ExtractedDocument::content` where the mention ends (exclusive).
          minimum: 0
        start:
          type: integer
          format: int32
          description: Byte-offset span in `ExtractedDocument::content` where the mention starts.
          minimum: 0
        text:
          type: string
          description: Raw mention text exactly as it appeared in the source.
    EntityCategory:
      oneOf:
        - type: string
          description: A person's name.
          enum:
            - person
        - type: string
          description: A company, institution, or organisation name.
          enum:
            - organization
        - type: string
          description: A geographic location (city, country, address).
          enum:
            - location
        - type: string
          description: A calendar date.
          enum:
            - date
        - type: string
          description: A time of day or duration.
          enum:
            - time
        - type: string
          description: A monetary amount with optional currency.
          enum:
            - money
        - type: string
          description: A percentage value.
          enum:
            - percent
        - type: string
          description: An email address.
          enum:
            - email
        - type: string
          description: A phone number.
          enum:
            - phone
        - type: string
          description: A URL or URI.
          enum:
            - url
        - type: object
          description: A caller-supplied custom category label.
          required:
            - custom
          properties:
            custom:
              type: string
              description: A caller-supplied custom category label.
      description: |-
        Standard entity categories produced by built-in NER backends.

        The `Custom(String)` variant lets caller-supplied categories (e.g. LLM
        schemas) flow through without losing fidelity to the consumer.
    EpubMetadata:
      type: object
      description: EPUB metadata (Dublin Core extensions).
      required:
        - format_type
      properties:
        cover_image:
          type:
            - string
            - "null"
          description: Path or identifier of the cover image within the EPUB container.
        coverage:
          type:
            - string
            - "null"
          description: Dublin Core `coverage` field (geographic or temporal scope).
        dc_format:
          type:
            - string
            - "null"
          description: Dublin Core `format` field (media type of the resource).
        dc_type:
          type:
            - string
            - "null"
          description: Dublin Core `type` field (nature or genre of the resource).
        format_type:
          type: string
          enum:
            - epub
        relation:
          type:
            - string
            - "null"
          description: Dublin Core `relation` field (related resource identifier).
        source:
          type:
            - string
            - "null"
          description: Dublin Core `source` field (origin resource identifier).
    ErrorBody:
      type: object
      description: Error body containing code, message, and optional details.
      required:
        - code
        - message
      properties:
        code:
          type: string
          description: Stable string error code (e.g., "rag.vector.dim_mismatch", "not_found")
          example: rag.vector.dim_mismatch
        details:
          type: object
          description: Optional detailed error information (context-specific)
        message:
          type: string
          description: Human-readable error message
          example: Caller-supplied embedding does not match collection dimension
    ErrorMetadata:
      type: object
      description: Error metadata (for batch operations).
      required:
        - error_type
        - message
      properties:
        error_type:
          type: string
          description: Machine-readable error type identifier (e.g. "UnsupportedFormat").
        message:
          type: string
          description: Human-readable error description.
    ErrorResponse:
      type: object
      description: |-
        Canonical error response body.

        Wraps the nested error envelope required by all API error paths.
      required:
        - error
      properties:
        error:
          $ref: '#/components/schemas/ErrorBody'
          description: The nested error body with code, message, and optional details.
    ExcelMetadata:
      type: object
      description: |-
        Excel/spreadsheet format metadata.

        Identifies the document as a spreadsheet source via the `FormatMetadata::Excel`
        discriminant. Sheet count and sheet names are stored inside this struct.
      required:
        - format_type
      properties:
        format_type:
          type: string
          enum:
            - excel
        sheet_count:
          type:
            - integer
            - "null"
          format: int32
          description: Number of sheets in the workbook.
          minimum: 0
        sheet_names:
          type:
            - array
            - "null"
          items:
            type: string
          description: Names of all sheets in the workbook.
    ExtractMultipartForm:
      type: object
      description: |-
        Multipart (`multipart/form-data`) variant of the extract request body.

        Send one or more files as binary parts whose field name is the filename
        (for example a part named `invoice.pdf`). Two reserved text parts carry
        JSON: `options` (an `ExtractionOptions`) and `webhook` (a
        `WebhookConfig`). To attach a lineage document id to a file, add a text
        part named `document_id-<filename>` whose value is the document UUID. To
        attach a per-file extraction config override, add a text part named
        `config-<filename>` whose value is a JSON-encoded `FileExtractionConfig`
        — see that schema's own description for the merge precedence against
        `options.extraction_config`.
      required:
        - file
      properties:
        file:
          type: array
          items:
            type: integer
            format: binary
            minimum: 0
          description: |-
            A document file. Repeat with one binary part per file; the part's field
            name (or filename) becomes the document filename and its `Content-Type`
            the document MIME type.
          contentMediaType: application/octet-stream
        options:
          $ref: '#/components/schemas/ExtractionOptions'
          description: Optional `options` part — a JSON-encoded `ExtractionOptions`.
        webhook:
          $ref: '#/components/schemas/WebhookConfig'
          description: |-
            Optional `webhook` part — a JSON-encoded `WebhookConfig` for async
            result delivery.
    ExtractRequest:
      type: object
      description: |-
        Request body for `POST /v1/extract`.

        Carries a document batch, a set of seed URLs to crawl, or both. `options`
        and `webhook` apply to every document and URL in the request (ADR-0066,
        amended 2026-09-05).
      properties:
        crawl_config:
          type: object
          description: |-
            Crawl configuration applied to every entry in `urls`, overridable
            per-URL. Kept as opaque JSON — see `UrlInput.crawl_config`. Sending
            it without any `urls` is a validation error rather than a no-op.
        documents:
          type: array
          items:
            $ref: '#/components/schemas/DocumentInput'
          description: Documents to extract inline or via an integration reference.
        options:
          $ref: '#/components/schemas/ExtractionOptions'
          description: Extraction options shared by every document/URL in this request.
        project_id:
          type:
            - string
            - "null"
          format: uuid
          description: |-
            Project the job is written into. Absent writes into the project the
            credential carries.

            A session token freezes its project at login, so a user who belongs to
            several projects needs a way to name the one the dashboard shows. The
            Pro service authorizes any other project against the projects the
            caller can see, exactly as the job reads do.

            The Enterprise API service cannot make that check — its credential
            names no user — so it rejects a value that names any project other
            than the one the credential carries.
        urls:
          type: array
          items:
            $ref: '#/components/schemas/UrlInput'
          description: |-
            URLs to crawl and extract documents from. Seed URLs that share one
            effective configuration become one crawl job.
        webhook:
          $ref: '#/components/schemas/WebhookConfig'
          description: Webhook configuration for async result delivery.
      additionalProperties: false
    ExtractResponse:
      type: object
      description: |-
        Response body for `POST /v1/extract` (HTTP 202 Accepted).

        `job_ids` and `documents` carry one entry per directly submitted document,
        and `crawl_job_ids` one per group of seed URLs sharing a configuration.
        Each list is omitted from the wire entirely when empty, so a request that
        submitted only documents carries no `crawl_job_ids` and vice versa.
      required:
        - status
      properties:
        crawl_job_ids:
          type: array
          items:
            type: string
          description: |-
            Crawl job IDs, one per group of seed URLs sharing a configuration.
            Each is the `crawl_job_id` of the crawl-event stream.
        documents:
          type: array
          items:
            $ref: '#/components/schemas/DocumentSubmission'
          description: |-
            Detailed per-document submissions, including lineage when present.
            Populated by both tiers; `document_id`/`version_sequence` stay `None`
            on Pro, which has no lineage feature.
        job_ids:
          type: array
          items:
            type: string
          description: Job IDs created for directly submitted documents.
        status:
          type: string
          description: Overall submission status (e.g. `"pending"`, `"processing"`).
    ExtractedDocument:
      type: object
      description: |-
        Document extracted by the core extraction pipeline.

        `extract` and `extract_batch` return an `ExtractionResult` envelope whose
        `results` field contains these per-document payloads.
      required:
        - content
        - mime_type
        - metadata
        - tables
      properties:
        annotations:
          type:
            - array
            - "null"
          items:
            $ref: '#/components/schemas/PdfAnnotation'
          description: |-
            PDF annotations extracted from the document.

            When annotation extraction is enabled via `PdfConfig::extract_annotations`,
            this field contains text notes, highlights, links, stamps, and other
            annotations found in PDF documents.
        children:
          type:
            - array
            - "null"
          items:
            $ref: '#/components/schemas/ArchiveEntry'
          description: |-
            Nested extraction results from archive contents.

            When extracting archives, each processable file inside produces its own
            full extraction result. Set to `None` for non-archive formats.
            Use `max_archive_depth` in config to control recursion depth.
        chunks:
          type:
            - array
            - "null"
          items:
            $ref: '#/components/schemas/Chunk'
          description: |-
            Text chunks when chunking is enabled.

            When chunking configuration is provided, the content is split into
            overlapping chunks for efficient processing. Each chunk contains the text,
            optional embeddings (if enabled), and metadata about its position.
        code_intelligence:
          description: |-
            Code intelligence results from tree-sitter analysis.

            Populated when extracting source code files with the `tree-sitter` feature.
            Contains metrics, structural analysis, imports/exports, comments,
            docstrings, symbols, diagnostics, and optionally chunked code segments.

            Stored as an opaque JSON value so that all language bindings (Go, Java,
            C#, …) can deserialize it as a raw JSON object rather than a typed struct.
            The underlying type is `tree_sitter_language_pack::ProcessResult`.
        content:
          type: string
          description: Plain-text representation of the extracted document content.
        counts:
          $ref: '#/components/schemas/DocumentCounts'
          description: |-
            Cheap structural counts (pages, tables, images).

            Always populated by the extraction pipeline, even when the `pages` /
            `images` collections are `None`. See `DocumentCounts`.
        detected_language_confidences:
          type:
            - array
            - "null"
          items:
            $ref: '#/components/schemas/LanguageConfidence'
          description: |-
            Structured per-language detection results: confidence, document share, script,
            and reliability, alongside the ISO-code-only `detected_languages` (#261).

            One entry per language in `detected_languages`, in the same order. `None` under
            the same conditions as `detected_languages`: detection disabled, empty input
            text, or no language met the configured `min_confidence`.
        detected_languages:
          type:
            - array
            - "null"
          items:
            type: string
          description: ISO 639-1 language codes detected in the document content.
        djot_content:
          $ref: '#/components/schemas/DjotContent'
          description: |-
            Rich Djot content structure (when extracting Djot documents).

            When extracting Djot documents with structured extraction enabled,
            this field contains the full semantic structure including:
            - Block-level elements with nesting
            - Inline formatting with attributes
            - Links, images, footnotes
            - Math expressions
            - Complete attribute information

            The `content` field still contains plain text for backward compatibility.

            Always `None` for non-Djot documents.
        document:
          $ref: '#/components/schemas/DocumentStructure'
          description: |-
            Structured document tree (when document structure extraction is enabled).

            When `include_document_structure` is true in `ExtractionConfig`, this field
            contains the full hierarchical representation of the document including:
            - Heading-driven section nesting
            - Table grids with cell-level metadata
            - Content layer classification (body, header, footer, footnote)
            - Inline text annotations (formatting, links)
            - Bounding boxes and page numbers

            Independent of `result_format` — can be combined with Unified or ElementBased.
        elements:
          type:
            - array
            - "null"
          items:
            $ref: '#/components/schemas/Element'
          description: |-
            Semantic elements when element-based result format is enabled.

            When result_format is set to ElementBased, this field contains semantic
            elements with type classification, unique identifiers, and metadata for
            Unstructured-compatible element-based processing.
        entities:
          type:
            - array
            - "null"
          items:
            $ref: '#/components/schemas/Entity'
          description: |-
            Named entities detected in `content` by the NER post-processor.

            `None` when no NER backend is configured. Populated by the `xberg-gliner`
            ONNX backend or the LLM-driven backend (see `crates/xberg/src/text/ner/`).
        extracted_keywords:
          type:
            - array
            - "null"
          items:
            $ref: '#/components/schemas/Keyword'
          description: |-
            Extracted keywords when keyword extraction is enabled.

            When keyword extraction (RAKE or YAKE) is configured, this field contains
            the extracted keywords with scores, algorithm info, and position data.
            Previously stored in `metadata.additional["keywords"]`.
        extraction_confidence:
          $ref: '#/components/schemas/ExtractionConfidence'
          description: |-
            Confidence score computed by the heuristics pipeline.

            Populated when the `heuristics` feature is enabled and confidence
            scoring has been performed.  Combines text-coverage, OCR aggregate
            confidence, and schema-compliance into a single `[0, 1]` value.

            `None` when confidence scoring is not configured or the feature is
            absent.
        extraction_method:
          $ref: '#/components/schemas/ExtractionMethod'
          description: |-
            Extraction strategy used to produce the returned text.

            Populated when the extractor can reliably distinguish native text extraction,
            OCR-only extraction, or mixed native/OCR output.
        form_fields:
          type: array
          items:
            $ref: '#/components/schemas/PdfFormField'
          description: |-
            Form fields extracted from a PDF's AcroForm or XFA structure.

            Populated by the PDF extractor when `PdfConfig::extract_form_fields` is
            enabled (default) and the document is a fillable form. Empty otherwise.
        formulas:
          type: array
          items:
            $ref: '#/components/schemas/Formula'
          description: |-
            Mathematical formulas recognized in the document.

            Populated from every source that produces formulas: layout-guided OCR
            (with geometry), VLM OCR (text only), and markup extraction (DOCX,
            PPTX, ODT, EPUB, HTML, JATS, LaTeX, Markdown, and related formats,
            without geometry). Empty when the document contains no formulas.
        images:
          type:
            - array
            - "null"
          items:
            $ref: '#/components/schemas/ExtractedImage'
          description: |-
            Extracted images from the document.

            When image extraction is enabled via `ImageExtractionConfig`, this field
            contains all images found in the document with their raw data and metadata.
            Each image may optionally contain a nested `ocr_result` if OCR was performed.
        llm_usage:
          type:
            - array
            - "null"
          items:
            $ref: '#/components/schemas/LlmUsage'
          description: |-
            LLM token usage and cost data for all LLM calls made during this extraction.

            Contains one entry per LLM call. Multiple entries are produced when
            VLM OCR, structured extraction, or LLM embeddings run during
            the same extraction.

            `None` when no LLM was used.
        metadata:
          $ref: '#/components/schemas/Metadata'
          description: Document-level metadata (author, title, dates, format-specific fields).
        mime_type:
          type: string
          description: MIME type of the source document (e.g. `"application/pdf"`).
        ocr_elements:
          type:
            - array
            - "null"
          items:
            $ref: '#/components/schemas/OcrElement'
          description: |-
            OCR elements with full spatial and confidence metadata.

            When OCR is performed with element extraction enabled, this field contains
            the structured representation of detected text including:
            - Bounding geometry (rectangles or quadrilaterals)
            - Confidence scores (detection and recognition)
            - Rotation information
            - Hierarchical relationships (Tesseract only)

            This field preserves all metadata that would otherwise be lost when
            converting to plain text or markdown output formats.

            Only populated when `OcrElementConfig.include_elements` is true.
        ocr_page_failures:
          type:
            - array
            - "null"
          items:
            $ref: '#/components/schemas/OcrPageFailure'
          description: |-
            Pages whose OCR failed while extraction continued, in the order the failures were reported.

            A record with `recovered` set means the document still has content for that page
            from native text or from embedded images. Xberg-produced results use `None` when
            no page failed.
        page_classifications:
          type:
            - array
            - "null"
          items:
            $ref: '#/components/schemas/PageClassification'
          description: |-
            Per-page classifications produced by the page-classification post-processor.

            `None` when classification is not configured.
        pages:
          type:
            - array
            - "null"
          items:
            $ref: '#/components/schemas/PageContent'
          description: |-
            Per-page content when page extraction is enabled.

            When page extraction is configured, the document is split into per-page content
            with tables and images mapped to their respective pages.
        processing_warnings:
          type: array
          items:
            $ref: '#/components/schemas/ProcessingWarning'
          description: |-
            Non-fatal warnings collected during processing pipeline stages.

            Captures errors from optional pipeline features (embedding, chunking,
            language detection, output formatting) that don't prevent extraction
            but may indicate degraded or incomplete results. These warnings are
            independent of `quality_score`, which assesses only retained text.
            Previously stored as individual keys in `metadata.additional`.
        quality_score:
          type:
            - number
            - "null"
          format: double
          description: |-
            Text cleanliness/readability score from quality analysis.

            A value between 0.0 and 1.0 describing the quality of the text that was
            retained. This is not a completeness or recall score: clean text can score
            highly even when an extractor omitted or rejected other content. Inspect
            `processing_warnings` separately for known degraded or partial extraction.

            When the text came from OCR and the result carries enough recognized words to
            judge, this score is additionally capped by the mean OCR recognition confidence.
            Text that looks clean but that OCR itself had little confidence in therefore
            cannot score high. A native, non-OCR extraction is not capped.
            Previously stored in `metadata.additional["quality_score"]`.
        redaction_report:
          $ref: '#/components/schemas/RedactionReport'
          description: |-
            Audit report of redactions applied by the redaction post-processor.

            The redaction processor rewrites `content`, `formatted_content`, every
            chunk's text, and the textual fields of `entities` / `summary` / `translation` /
            `page_classifications` in place. This report describes what was found and how it
            was replaced. `None` when redaction is not configured.
        revisions:
          type:
            - array
            - "null"
          items:
            $ref: '#/components/schemas/DocumentRevision'
          description: |-
            Tracked changes embedded in the source document.

            Populated by per-format extractors that understand change-tracking
            metadata (DOCX `w:ins`/`w:del`/`w:rPrChange`, ODT `text:change-*`,
            …). Every extractor defaults to `None` until its format-specific
            implementation is added. Extractors that do populate this field follow
            the "accepted-changes" convention: inserted text is present in
            `content`, deleted text is absent — the revision list is the separate
            audit trail.
        structured_output:
          description: |-
            Structured extraction output from LLM-based JSON schema extraction.

            When `structured_extraction` is configured in `ExtractionConfig`, the
            extracted document content is sent to a VLM with the provided JSON schema.
            The response is parsed and stored here as a JSON value matching the schema.
        summary:
          $ref: '#/components/schemas/DocumentSummary'
          description: |-
            Summary of `content` produced by the summarisation post-processor.

            `None` when summarisation is not configured. Populated by the TextRank
            extractive backend (deterministic, no external service) or by the
            liter-llm-driven abstractive backend.
        tables:
          type: array
          items:
            $ref: '#/components/schemas/Table'
          description: Tables extracted from the document, each with structured cell data.
        translation:
          $ref: '#/components/schemas/Translation'
          description: |-
            Translation of `content` produced by the translation post-processor.

            `None` when translation is not configured.
        uris:
          type:
            - array
            - "null"
          items:
            $ref: '#/components/schemas/ExtractedUri'
          description: |-
            URIs/links discovered during document extraction.

            Contains hyperlinks, image references, citations, email addresses, and
            other URI-like references found in the document. Always extracted when
            present in the source document.
    ExtractedImage:
      type: object
      description: |-
        Extracted image from a document.

        Contains raw image data, metadata, and optional nested OCR results.
        Raw bytes allow cross-language compatibility - users can convert to
        PIL.Image (Python), Sharp (Node.js), or other formats as needed.
      required:
        - data
        - format
        - image_index
      properties:
        bits_per_component:
          type:
            - integer
            - "null"
          format: int32
          description: Bits per color component (e.g., 8, 16)
          minimum: 0
        bounding_box:
          $ref: '#/components/schemas/BoundingBox'
          description: |-
            Bounding box of the image on the page (PDF coordinates: x0=left, y0=bottom, x1=right, y1=top).
            Only populated for PDF-extracted images when position data is available from the PDF extractor.
        caption:
          type:
            - string
            - "null"
          description: |-
            VLM-generated caption describing the image, when captioning is configured.

            Populated by the captioning post-processor
            (`crates/xberg/src/plugins/processor/builtin/captioning.rs`), which routes
            each image through `crate::llm::region_extractor::extract_region_with_vlm` in
            caption mode. `None` when captioning is disabled or the VLM declined to caption.
        cluster_id:
          type:
            - integer
            - "null"
          format: int32
          description: |-
            Identifier shared across images that form a single logical figure
            (e.g. all raster tiles of one technical drawing). `None` for singletons.
          minimum: 0
        colorspace:
          type:
            - string
            - "null"
          description: Colorspace information (e.g., "RGB", "CMYK", "Gray")
        data:
          type: array
          items:
            type: integer
            format: binary
            minimum: 0
          description: |-
            Raw image data (PNG, JPEG, WebP, etc. bytes).
            Uses `bytes::Bytes` for cheap cloning of large buffers.
        data_base64:
          type:
            - string
            - "null"
          description: |-
            Base64-encoded copy of `data`; populated when `ImageExtractionConfig::include_data_base64`
            is `true`. Omitted from JSON by default; use instead of `data` in JSON-only clients.
        description:
          type:
            - string
            - "null"
          description: Optional description of the image
        format:
          type: string
          description: |-
            Image format (e.g., "jpeg", "png", "webp")
            Uses Cow<'static, str> to avoid allocation for static literals.
        height:
          type:
            - integer
            - "null"
          format: int32
          description: Image height in pixels
          minimum: 0
        image_index:
          type: integer
          format: int32
          description: Zero-indexed position of this image in the document/page
          minimum: 0
        image_kind:
          $ref: '#/components/schemas/ImageKind'
          description: |-
            Heuristic classification of what this image likely depicts.
            `None` if classification was disabled or inconclusive.
        is_mask:
          type: boolean
          description: Whether this image is a mask image
        kind_confidence:
          type:
            - number
            - "null"
          format: float
          description: Confidence score for `image_kind`, in the range 0.0 to 1.0.
        ocr_result:
          $ref: '#/components/schemas/ExtractedDocument'
          description: |-
            Nested OCR extraction result (if image was OCRed)

            When OCR is performed on this image, the result is embedded here
            rather than in a separate collection, making the relationship explicit.
        page_number:
          type:
            - integer
            - "null"
          format: int32
          description: Page/slide number where image was found (1-indexed)
          minimum: 0
        qr_codes:
          type:
            - array
            - "null"
          items:
            $ref: '#/components/schemas/QrCode'
          description: |-
            QR codes decoded from this image, when QR detection is enabled.

            Populated by the QR post-processor (`crates/xberg/src/extractors/qr.rs`) via
            the pure-Rust `rqrr` decoder. `None` when QR detection is disabled; an empty
            `Some(vec![])` when detection ran but found nothing.
        source_path:
          type:
            - string
            - "null"
          description: |-
            Original source path of the image within the document archive (e.g., "media/image1.png" in DOCX).
            Used for rendering image references when the binary data is not extracted.
        width:
          type:
            - integer
            - "null"
          format: int32
          description: Image width in pixels
          minimum: 0
    ExtractedUri:
      type: object
      description: |-
        A URI extracted from a document.

        Represents any link, reference, or resource pointer found during extraction.
        The `kind` field classifies the URI semantically, while `label` carries
        optional human-readable display text.
      required:
        - url
        - kind
      properties:
        kind:
          $ref: '#/components/schemas/UriKind'
          description: Semantic classification of the URI.
        label:
          type:
            - string
            - "null"
          description: Optional display text / label for the link.
        page:
          type:
            - integer
            - "null"
          format: int32
          description: Optional page number where the URI was found (1-indexed).
          minimum: 0
        url:
          type: string
          description: The URL or path string.
    ExtractionConfidence:
      type: object
      description: |-
        Combined confidence on `[0, 1]`.

        When OCR did not run, the `ocr_aggregate` weight folds into `text_coverage`
        so the weighted sum still totals 1.0.
      required:
        - text_coverage
        - schema_compliance
        - combined
      properties:
        combined:
          type: number
          format: float
          description: Weighted blend in `[0, 1]`.  The value compared against the fallback threshold.
        ocr_aggregate:
          type:
            - number
            - "null"
          format: float
          description: |-
            OCR recognition confidence, word-count-weighted across every recognized word, when OCR
            ran; `None` when it did not.
        schema_compliance:
          $ref: '#/components/schemas/SchemaCompliance'
          description: Whether the merged output validates against the preset schema.
        text_coverage:
          type: number
          format: float
          description: Fraction of pages with a usable text layer.
    ExtractionConfig:
      type: object
      description: Top-level extraction configuration — mirrors xberg::ExtractionConfig.
      properties:
        cache_namespace:
          type:
            - string
            - "null"
          description: Cache namespace for tenant isolation
        cache_ttl_secs:
          type:
            - integer
            - "null"
          format: int64
          description: Per-request cache TTL override in seconds
          minimum: 0
        chunking:
          $ref: '#/components/schemas/ChunkingConfig'
          description: Text chunking configuration
        content_filter:
          $ref: '#/components/schemas/ContentFilterConfig'
          description: Content filtering (headers, footers, watermarks)
        disable_ocr:
          type:
            - boolean
            - "null"
          description: |-
            Disable OCR entirely. `true` conflicts with an `ocr` block, including
            one inherited by a per-file override.
        enable_quality_processing:
          type:
            - boolean
            - "null"
          description: Enable quality post-processing. Defaults to true when omitted.
        extraction_timeout_secs:
          type:
            - integer
            - "null"
          format: int64
          description: |-
            Extraction timeout in seconds, applied to every document in the batch
            unless a per-file `FileExtractionConfig.extraction_timeout_secs`
            overrides it (default: 600, min: 1, max: 600).

            The engine clamps a client-supplied value lower-only against its own
            default (`crates/engine/src/config_convert.rs`'s `wire_to_xberg`): a
            value above 600 is silently reduced to 600, never rejected. `maximum`
            here documents that existing ceiling for the SDK rather than enforcing
            a second one — this field is otherwise unvalidated at the API boundary.
          example: 600
          maximum: 600
          minimum: 1
        force_ocr:
          type:
            - boolean
            - "null"
          description: Force OCR on all pages (bypass native text extraction)
        force_ocr_pages:
          type:
            - array
            - "null"
          items:
            type: integer
            format: int32
            minimum: 0
          description: Force OCR on specific pages (1-indexed)
        html_options:
          type: object
          description: HTML extraction options (flexible JSON)
        images:
          $ref: '#/components/schemas/ImageExtractionConfig'
          description: Image extraction configuration
        include_document_structure:
          type:
            - boolean
            - "null"
          description: Include structured document tree in output
        keywords:
          $ref: '#/components/schemas/KeywordConfig'
          description: Keyword extraction configuration
        language_detection:
          $ref: '#/components/schemas/LanguageDetectionConfig'
          description: Language detection configuration
        layout:
          $ref: '#/components/schemas/LayoutDetectionConfig'
          description: Layout detection configuration
        max_archive_depth:
          type:
            - integer
            - "null"
          format: int32
          description: Maximum recursion depth for archive extraction
          minimum: 0
        max_concurrent_extractions:
          type:
            - integer
            - "null"
          format: int32
          description: Maximum concurrent extractions
          minimum: 0
        max_embedded_file_bytes:
          type:
            - integer
            - "null"
          format: int64
          description: Maximum size in bytes for embedded/attached files during recursive extraction
          minimum: 0
        ocr:
          $ref: '#/components/schemas/OcrConfig'
          description: OCR configuration
        ocr_strategy:
          $ref: '#/components/schemas/OcrStrategy'
          description: |-
            Which pages go to OCR when neither `force_ocr` nor `force_ocr_pages`
            applies. Omitting it keeps xberg's default, `auto`.
        output_format:
          type:
            - string
            - "null"
          description: 'Output text format: "plain", "markdown", "html", "djot", "structured", "json"'
        pages:
          $ref: '#/components/schemas/PageConfig'
          description: Page extraction configuration
        pdf_options:
          $ref: '#/components/schemas/PdfConfig'
          description: PDF-specific options
        postprocessor:
          $ref: '#/components/schemas/PostProcessorConfig'
          description: Post-processor configuration
        result_format:
          type:
            - string
            - "null"
          description: 'Result format: "unified" or "element_based"'
        security_limits:
          $ref: '#/components/schemas/SecurityLimits'
          description: |-
            Security limits for extraction (resource caps, recursion bounds)

            Carried as the client's raw JSON on purpose. Typing it as
            `xberg::SecurityLimits` dropped every unknown key at DTO parse, so
            `validate_via_shared_conversion` — which re-serializes this struct —
            only ever saw a blob that was already clean, and a caller who misspelled
            a limit got a 200 and an extraction that ignored it. Keeping the raw
            value means both tiers reach the same unknown-key check and the same
            clamp in `engine::config_convert`. `value_type` keeps the published
            schema unchanged.
        structured_extraction:
          $ref: '#/components/schemas/StructuredExtractionConfig'
          description: 'LLM-driven structured extraction: schema + provider config.'
        token_reduction:
          $ref: '#/components/schemas/TokenReductionConfig'
          description: Token reduction configuration
        use_cache:
          type:
            - boolean
            - "null"
          description: Enable extraction result caching
      additionalProperties: false
    ExtractionEventSummary:
      type: object
      description: Summary of a structured extraction event.
      required:
        - id
        - job_id
        - preset_id
        - preset_version
        - call_mode
        - vision_call_count
        - page_count
        - outcome
        - created_at
      properties:
        call_mode:
          type: string
          description: Call mode used (text_only, vision_only, or text_plus_vision).
        created_at:
          type: string
          format: date-time
          description: ISO 8601 creation timestamp.
        dominant_model:
          type:
            - string
            - "null"
          description: Dominant LLM model used.
        error_message:
          type:
            - string
            - "null"
          description: Error message if outcome is error.
        fallback_used:
          type:
            - boolean
            - "null"
          description: Whether a fallback path was taken.
        final_confidence:
          type:
            - number
            - "null"
          format: float
          description: Final confidence score after any fallback.
        id:
          type: string
          format: uuid
          description: Unique event ID.
        job_id:
          type: string
          format: uuid
          description: Associated job ID.
        outcome:
          type: string
          description: 'Outcome: success, partial_success, schema_invalid, or error.'
        page_count:
          type: integer
          format: int32
          description: Number of pages processed.
        preset_id:
          type: string
          description: Preset identifier (slug, not a UUID).
        preset_version:
          type: string
          description: Preset version.
        text_confidence:
          type:
            - number
            - "null"
          format: float
          description: Text-only confidence score (if available).
        total_cost_usd_cents:
          type:
            - integer
            - "null"
          format: int64
          description: Total cost in USD cents (if call succeeded).
        total_input_tokens:
          type:
            - integer
            - "null"
          format: int64
          description: Total input tokens (if call succeeded).
        total_output_tokens:
          type:
            - integer
            - "null"
          format: int64
          description: Total output tokens (if call succeeded).
        vision_call_count:
          type: integer
          format: int32
          description: Number of vision API calls made.
    ExtractionJobResponse:
      type: object
      description: Metadata for one extraction job returned by `GET /v1/jobs/{id}`.
      required:
        - id
        - filename
        - mime_type
        - status
        - created_at
      properties:
        cached:
          type:
            - boolean
            - "null"
          description: Whether the result came from the server-side cache, when known.
        child_job_ids:
          type: array
          items:
            type: string
          description: Child extraction jobs created by document splitting.
        completed_at:
          type:
            - string
            - "null"
          description: ISO 8601 completion timestamp, when available.
        created_at:
          type: string
          format: date-time
          description: ISO 8601 creation timestamp.
        error:
          type:
            - string
            - "null"
          description: Failure reason for a failed job.
        filename:
          type: string
          description: Original filename.
        id:
          type: string
          format: uuid
          description: Unique job identifier.
        images_extracted:
          type:
            - integer
            - "null"
          format: int32
          description: Images extracted, when recorded by the tier's persistence layer.
        mime_type:
          type: string
          description: MIME type of the submitted document.
        pages_extracted:
          type:
            - integer
            - "null"
          format: int32
          description: Pages extracted, when recorded by the tier's persistence layer.
        parent_crawl_job_id:
          type:
            - string
            - "null"
          format: uuid
          description: The crawl job that spawned this job as a child, when any.
        processing_time_ms:
          type:
            - integer
            - "null"
          format: int64
          description: Server-side processing duration in milliseconds.
        result:
          $ref: '#/components/schemas/ExtractedDocument'
          description: |-
            Extraction result embedded by Enterprise for compatibility. Clients may
            use `GET /v1/jobs/{id}/result` on either tier instead.
        source_url:
          type:
            - string
            - "null"
          description: |-
            The crawled page URL this document came from, when the job is a
            crawl child (#1961). Absent for a direct upload.
        status:
          $ref: '#/components/schemas/JobStatus'
          description: Current job status.
        tables_extracted:
          type:
            - integer
            - "null"
          format: int32
          description: Tables extracted, when recorded by the tier's persistence layer.
        webhook_delivery:
          $ref: '#/components/schemas/WebhookDelivery'
          description: Inline webhook delivery state, when this job configured one.
    ExtractionMethod:
      type: string
      description: How the extracted text was produced.
      enum:
        - native
        - ocr
        - mixed
    ExtractionOptions:
      type: object
      description: Extraction options shared across every document in a request.
      properties:
        context:
          type:
            - object
            - "null"
          description: |-
            Free-form context merged into the preset's context template before
            the system prompt is sent to the model. Ignored when `preset` and
            `saved_preset_id` are both unset.
          additionalProperties:
            type: string
          propertyNames:
            type: string
        extraction_config:
          type: object
          description: |-
            Extraction configuration as opaque JSON. Kept untyped here: the
            concrete schema (`services/api::rest::types::ExtractionConfig` on
            Enterprise, a raw `serde_json::Value` on Pro) is service-local and out
            of scope for this convergence pass.
            Both tiers reject unknown extraction-config fields and reject an
            effective config with `disable_ocr: true` alongside an `ocr` block.
        preset:
          type:
            - string
            - "null"
          description: |-
            Curated preset identifier. Mutually exclusive with `saved_preset_id`
            and with an inline schema in `extraction_config`.
        saved_preset_id:
          type:
            - string
            - "null"
          description: |-
            Saved preset ID (project-scoped, user-created). Mutually exclusive
            with `preset`.

            Both tiers apply it. The Enterprise extraction handler (`extract` in
            `services/api/src/rest/extract.rs`) fetches the saved preset through
            the project-scoped connection after the project is authorized and
            applies its schema and system prompt before any job exists, the same
            way Pro injects it, so both tiers dispatch the same config. An unknown
            id is a validation error.
        split_documents:
          type:
            - boolean
            - "null"
          description: |-
            When true and a document is a PDF, enable multi-document boundary
            detection and fan-out orchestration. Enterprise-only. Xberg Pro has no
            splitter and returns 400 when `split_documents` is true.
        tuning_profile_id:
          type:
            - string
            - "null"
          description: |-
            Apply a promoted Auto-Tune profile to Pro document extraction, by its id
            from `GET /v1/tuning-profiles`. Enterprise and Pro requests containing
            crawl URLs reject this option instead of silently ignoring it.

            The profile supplies each document's starting configuration, routed by
            that document's own features, so a profile learned with routing on
            applies its routes rather than only its global config. A per-document
            `config` override still wins, as it does over every other source.

            Mutually exclusive with `preset`, `saved_preset_id` and a request-level
            `extraction_config`: each of those is another answer to the same
            question, and silently picking one would make the winner invisible.
      additionalProperties: false
    FictionBookMetadata:
      type: object
      description: FictionBook (FB2) metadata.
      required:
        - format_type
      properties:
        annotation:
          type:
            - string
            - "null"
          description: Short annotation / summary from the FB2 `<annotation>` element.
        format_type:
          type: string
          enum:
            - fiction_book
        genres:
          type: array
          items:
            type: string
          description: Genre tags as declared in the FB2 `<genre>` elements.
        sequences:
          type: array
          items:
            type: string
          description: Book series (sequence) names, if any.
    FileExtractionConfig:
      type: object
      description: |-
        Per-file extraction config override for batch/presign requests.

        All fields are optional — only provided fields override the batch-level config.
        Batch-only fields (`use_cache`, `cache_namespace`, `cache_ttl_secs`,
        `max_concurrent_extractions`, `security_limits`, `max_archive_depth`,
        `max_embedded_file_bytes`) are not per-file overrides.

        Precedence, highest to lowest: this per-file config > the request's
        batch-level `options.extraction_config` > a curated/saved preset > the
        project default. The preset and project default are already folded into
        the batch-level config by the time a per-file override is applied, and the
        merge is shallow over top-level fields, so an override here always wins the
        field it sets and an unset field falls through to whatever the batch level
        carries.
      properties:
        chunking:
          $ref: '#/components/schemas/ChunkingConfig'
        content_filter:
          $ref: '#/components/schemas/ContentFilterConfig'
        disable_ocr:
          type:
            - boolean
            - "null"
          description: |-
            `true` conflicts with an `ocr` block, including one inherited from
            the batch configuration.
        enable_quality_processing:
          type:
            - boolean
            - "null"
        extraction_timeout_secs:
          type:
            - integer
            - "null"
          format: int64
          description: |-
            Per-file extraction timeout in seconds (default: 600, min: 1, max: 600).

            The engine clamps a client-supplied value lower-only against its own
            default (`crates/engine/src/config_convert.rs`'s `wire_to_xberg`, which
            `wire_to_xberg_file` delegates to for a per-file override): a value
            above 600 is silently reduced to 600, never rejected. `maximum` here
            documents that existing ceiling for the SDK rather than enforcing a
            second one — this field is otherwise unvalidated at the API boundary.
          example: 600
          maximum: 600
          minimum: 1
        force_ocr:
          type:
            - boolean
            - "null"
        force_ocr_pages:
          type:
            - array
            - "null"
          items:
            type: integer
            format: int32
            minimum: 0
        html_options:
          type: object
        images:
          $ref: '#/components/schemas/ImageExtractionConfig'
        include_document_structure:
          type:
            - boolean
            - "null"
        keywords:
          $ref: '#/components/schemas/KeywordConfig'
        language_detection:
          $ref: '#/components/schemas/LanguageDetectionConfig'
        layout:
          $ref: '#/components/schemas/LayoutDetectionConfig'
        ocr:
          $ref: '#/components/schemas/OcrConfig'
        ocr_strategy:
          $ref: '#/components/schemas/OcrStrategy'
          description: |-
            Which pages go to OCR when neither `force_ocr` nor `force_ocr_pages`
            applies. Omitting it keeps xberg's default, `auto`.
        output_format:
          type:
            - string
            - "null"
          description: |-
            Rendered output format: `plain` or `text`, `markdown` or `md`, `djot`,
            `html`, `json`, or `doctags`. Any other value is refused with a 400.
        pages:
          $ref: '#/components/schemas/PageConfig'
        pdf_options:
          $ref: '#/components/schemas/PdfConfig'
        postprocessor:
          $ref: '#/components/schemas/PostProcessorConfig'
        result_format:
          type:
            - string
            - "null"
        structured_extraction:
          $ref: '#/components/schemas/StructuredExtractionConfig'
        token_reduction:
          $ref: '#/components/schemas/TokenReductionConfig'
      additionalProperties: false
    Footnote:
      type: object
      description: Footnote in Djot.
      required:
        - label
        - content
      properties:
        content:
          type: array
          items:
            $ref: '#/components/schemas/FormattedBlock'
          description: Footnote content blocks
        label:
          type: string
          description: Footnote label
    FormFieldType:
      type: string
      description: |-
        Kind of a PDF form field.

        Mirrors `xberg_native_pdf`'s widget field taxonomy without leaking the upstream
        type across the binding surface.
      enum:
        - text
        - checkbox
        - radio
        - choice
        - signature
        - button
        - unknown
    FormatMetadata:
      oneOf:
        - $ref: '#/components/schemas/PdfMetadata'
        - $ref: '#/components/schemas/DocxMetadata'
        - $ref: '#/components/schemas/ExcelMetadata'
        - $ref: '#/components/schemas/EmailMetadata'
        - $ref: '#/components/schemas/PptxMetadata'
        - $ref: '#/components/schemas/ArchiveMetadata'
        - $ref: '#/components/schemas/ImageMetadata'
        - $ref: '#/components/schemas/XmlMetadata'
        - $ref: '#/components/schemas/TextMetadata'
        - $ref: '#/components/schemas/HtmlMetadata'
        - $ref: '#/components/schemas/OcrMetadata'
        - $ref: '#/components/schemas/CsvMetadata'
        - $ref: '#/components/schemas/BibtexMetadata'
        - $ref: '#/components/schemas/CitationMetadata'
        - $ref: '#/components/schemas/FictionBookMetadata'
        - $ref: '#/components/schemas/DbfMetadata'
        - $ref: '#/components/schemas/EpubMetadata'
        - $ref: '#/components/schemas/JatsMetadata'
        - $ref: '#/components/schemas/PstMetadata'
        - $ref: '#/components/schemas/AudioMetadata'
        - $ref: '#/components/schemas/CodeMetadata'
      description: Format-specific metadata (discriminated union). Only one format type can exist per extraction result.
      discriminator:
        propertyName: format_type
        mapping:
          archive: '#/components/schemas/ArchiveMetadata'
          audio: '#/components/schemas/AudioMetadata'
          bibtex: '#/components/schemas/BibtexMetadata'
          citation: '#/components/schemas/CitationMetadata'
          code: '#/components/schemas/CodeMetadata'
          csv: '#/components/schemas/CsvMetadata'
          dbf: '#/components/schemas/DbfMetadata'
          docx: '#/components/schemas/DocxMetadata'
          email: '#/components/schemas/EmailMetadata'
          epub: '#/components/schemas/EpubMetadata'
          excel: '#/components/schemas/ExcelMetadata'
          fiction_book: '#/components/schemas/FictionBookMetadata'
          html: '#/components/schemas/HtmlMetadata'
          image: '#/components/schemas/ImageMetadata'
          jats: '#/components/schemas/JatsMetadata'
          ocr: '#/components/schemas/OcrMetadata'
          pdf: '#/components/schemas/PdfMetadata'
          pptx: '#/components/schemas/PptxMetadata'
          pst: '#/components/schemas/PstMetadata'
          text: '#/components/schemas/TextMetadata'
          xml: '#/components/schemas/XmlMetadata'
    FormattedBlock:
      type: object
      description: |-
        Block-level element in a Djot document.

        Represents structural elements like headings, paragraphs, lists, code blocks, etc.
      required:
        - block_type
        - inline_content
      properties:
        attributes:
          $ref: '#/components/schemas/Attributes'
          description: Element attributes (classes, IDs, key-value pairs)
        block_type:
          $ref: '#/components/schemas/BlockType'
          description: Type of block element
        children:
          type: array
          items:
            $ref: '#/components/schemas/FormattedBlock'
          description: Nested blocks for containers (blockquotes, list items, divs)
        code:
          type:
            - string
            - "null"
          description: Raw code content for code blocks
        inline_content:
          type: array
          items:
            $ref: '#/components/schemas/InlineElement'
          description: Inline content within the block
        language:
          type:
            - string
            - "null"
          description: Language identifier for code blocks
        level:
          type:
            - integer
            - "null"
          description: Heading level (1-6) for headings, or nesting level for lists
          minimum: 0
    Formula:
      type: object
      description: |-
        A mathematical formula extracted from a document.

        Three kinds of sources populate this type. Layout-guided OCR detects
        formula regions and recognizes them; those formulas carry a `bbox` and a
        `page`. VLM OCR recognizes formulas in transcribed text without layout, so
        its formulas carry no geometry. Markup extraction (DOCX, PPTX, ODT, EPUB,
        HTML, JATS, LaTeX, Markdown, and related formats) converts embedded math
        to LaTeX, also without geometry.
      required:
        - latex
      properties:
        bbox:
          $ref: '#/components/schemas/BoundingBox'
          description: |-
            Bounding box of the formula region on its page. `None` for markup sources.

            PDF OCR sources report PDF point coordinates with the origin at the
            bottom-left of the page, comparable to native PDF geometry. Image
            sources, and PDF pages whose geometry is unavailable, report pixels of
            the image the OCR backend saw. The C FFI reports an absent bbox as a
            null pointer.
        latex:
          type: string
          description: |-
            LaTeX source of the formula, without surrounding `$$` delimiters.

            Markup converters and formula OCR produce real LaTeX. The native PDF
            layout path stores the plain text of a detected formula region, which
            keeps the original Unicode math characters instead of LaTeX commands.
            To render the formula in Markdown or other formats, wrap it in `$$..$$`.
        page:
          type:
            - integer
            - "null"
          format: int32
          description: |-
            1-indexed page number the formula appears on. `None` when the source
            format has no page concept. The C FFI reports an absent page as `0`.
          minimum: 0
    GetJobResponse:
      oneOf:
        - allOf:
            - $ref: '#/components/schemas/ExtractionJobResponse'
              description: An extraction job.
            - type: object
              required:
                - job_type
              properties:
                job_type:
                  type: string
                  enum:
                    - extraction
          description: An extraction job.
        - allOf:
            - $ref: '#/components/schemas/CrawlJobResponse'
              description: A crawl job.
            - type: object
              required:
                - job_type
              properties:
                job_type:
                  type: string
                  enum:
                    - crawl
          description: A crawl job.
      description: |-
        Canonical response for `GET /v1/jobs/{id}` on both product tiers.

        `job_type` is the stable wire discriminator. Keeping selection explicit
        lets clients accept future fields without making the variants ambiguous.
    GridCell:
      type: object
      description: Individual grid cell with position and span metadata.
      required:
        - content
        - row
        - col
      properties:
        bbox:
          $ref: '#/components/schemas/BoundingBox'
          description: Bounding box for this cell (if available).
        col:
          type: integer
          format: int32
          description: Zero-indexed column position.
          minimum: 0
        col_span:
          type: integer
          format: int32
          description: Number of columns this cell spans.
          minimum: 0
        content:
          type: string
          description: Cell text content.
        heading_level:
          type:
            - integer
            - "null"
          format: int32
          description: |-
            Outline level (1-6) of the heading style this cell's text carries, when it has one.

            A DOCX banner row -- row 0, one cell spanning the grid, styled `Heading1`..`Heading6` --
            is what Word's navigation pane and a `TOC` field treat as the document's outline, but as
            a table cell it reached consumers as anonymous text (GH#1587). `content` is deliberately
            left as the bare cell text: prefixing it with `#` would put a markdown heading inside a
            table cell, which is invalid where it lands and changes text every existing consumer
            already reads. This field is the signal instead, so a caller can decide for itself
            whether a `heading 2` in a banner row is a section title or a column label.
          minimum: 0
        is_header:
          type: boolean
          description: Whether this is a header cell.
        row:
          type: integer
          format: int32
          description: Zero-indexed row position.
          minimum: 0
        row_span:
          type: integer
          format: int32
          description: Number of rows this cell spans.
          minimum: 0
        style_name:
          type:
            - string
            - "null"
          description: |-
            Human-readable name of the paragraph style applied to this cell's text (`heading 2`).

            Carries the style even when it resolves to no outline level, so a caller can key on a
            named style this crate does not map to a heading. See `GridCell.heading_level`.
    GroundTruthKind:
      type: string
      description: Kind of ground truth supplied for a document in an auto-tune run.
      enum:
        - markdown
        - plaintext
        - structured
    HeaderMetadata:
      type: object
      description: Header/heading element metadata.
      required:
        - level
        - text
        - depth
        - html_offset
      properties:
        depth:
          type: integer
          format: int32
          description: Document tree depth at the header element
          minimum: 0
        html_offset:
          type: integer
          format: int32
          description: Byte offset in original HTML document
          minimum: 0
        id:
          type:
            - string
            - "null"
          description: HTML id attribute if present
        level:
          type: integer
          format: int32
          description: 'Header level: 1 (h1) through 6 (h6)'
          minimum: 0
        text:
          type: string
          description: Normalized text content of the header
    HeadingContext:
      type: object
      description: |-
        Heading context for a chunk within a Markdown document.

        Contains the heading hierarchy from document root to this chunk's section.
      required:
        - headings
      properties:
        headings:
          type: array
          items:
            $ref: '#/components/schemas/HeadingLevel'
          description: |-
            The heading hierarchy from document root to this chunk's section.
            Index 0 is the outermost (h1), last element is the most specific.
    HeadingLevel:
      type: object
      description: A single heading in the hierarchy.
      required:
        - level
        - text
      properties:
        level:
          type: integer
          format: int32
          description: Heading depth (1 = h1, 2 = h2, etc.)
          minimum: 0
        text:
          type: string
          description: The text content of the heading.
    HealthResponse:
      type: object
      description: Liveness and deployment feature availability.
      required:
        - status
        - tier
        - auto_tune
        - rag
      properties:
        auto_tune:
          $ref: '#/components/schemas/AutoTuneHealth'
          description: Auto-tune readiness, read by the frontend to gate the auto-tune UI.
        rag:
          allOf:
            - type: object
              description: Enterprise RAG availability; project configuration remains authoritative.
              required:
                - enabled
              properties:
                enabled:
                  type: boolean
                  description: Always true because Enterprise mounts the RAG API unconditionally.
            - type: object
              description: RAG API availability, not a grant of access to a project.
        status:
          type: string
          description: Service status
          example: ok
        tier:
          type: string
          description: Deployment tier the frontend uses for runtime feature gating (ADR-0047).
          example: enterprise
    HierarchicalBlock:
      type: object
      required:
        - text
        - font_size
        - level
      properties:
        bbox:
          $ref: '#/components/schemas/HierarchicalBoundingBox'
          description: |-
            Bounding box information for the block

            Contains left, top, right, and bottom coordinates in PDF units.
        font_size:
          type: number
          format: float
          description: The font size of the text in this block
        level:
          type: string
          description: |-
            The hierarchy level of this block (H1-H6 or Body)

            Levels correspond to HTML heading tags:
            - "h1": Top-level heading
            - "h2": Secondary heading
            - "h3": Tertiary heading
            - "h4": Quaternary heading
            - "h5": Quinary heading
            - "h6": Senary heading
            - "body": Body text (no heading level)
        text:
          type: string
          description: The text content of this block
    HierarchicalBoundingBox:
      type: object
      required:
        - left
        - top
        - right
        - bottom
      properties:
        bottom:
          type: number
        left:
          type: number
        right:
          type: number
        top:
          type: number
    HierarchyConfig:
      type: object
      description: Heading hierarchy detection configuration.
      properties:
        enabled:
          type:
            - boolean
            - "null"
          description: Enable hierarchy detection. Defaults to true when omitted.
        include_bbox:
          type:
            - boolean
            - "null"
          description: Include bounding boxes. Defaults to true when omitted.
        k_clusters:
          type:
            - integer
            - "null"
          format: int32
          description: Number of font-size clusters for heading levels. Defaults to 3 when omitted.
          minimum: 0
        ocr_coverage_threshold:
          type:
            - number
            - "null"
          format: float
          description: Unsupported compatibility field. Non-null values are not supported by the extraction engine; omit or send null.
      additionalProperties: false
    HtmlMetadata:
      type: object
      description: |-
        HTML metadata extracted from HTML documents.

        Includes document-level metadata, Open Graph data, Twitter Card metadata,
        and extracted structural elements (headers, links, images, structured data).
      required:
        - format_type
      properties:
        author:
          type:
            - string
            - "null"
          description: Document author from `<meta name="author">` tag
        base_href:
          type:
            - string
            - "null"
          description: Base URL from `<base href="">` tag for resolving relative URLs
        canonical_url:
          type:
            - string
            - "null"
          description: Canonical URL from `<link rel="canonical">` tag
        description:
          type:
            - string
            - "null"
          description: Document description from `<meta name="description">` tag
        format_type:
          type: string
          enum:
            - html
        headers:
          type: array
          items:
            $ref: '#/components/schemas/HeaderMetadata'
          description: Extracted header elements with hierarchy
        images:
          type: array
          items:
            $ref: '#/components/schemas/ImageMetadataType'
          description: Extracted images with source and dimensions
        keywords:
          type: array
          items:
            type: string
          description: Document keywords from `<meta name="keywords">` tag, split on commas
        language:
          type:
            - string
            - "null"
          description: Document language from `lang` attribute
        links:
          type: array
          items:
            $ref: '#/components/schemas/LinkMetadata'
          description: Extracted hyperlinks with type classification
        meta_tags:
          type: object
          description: |-
            Additional meta tags not covered by specific fields
            Keys are meta name/property attributes, values are content
          additionalProperties:
            type: string
          propertyNames:
            type: string
        open_graph:
          type: object
          description: |-
            Open Graph metadata (og:* properties) for social media
            Keys like "title", "description", "image", "url", etc.
          additionalProperties:
            type: string
          propertyNames:
            type: string
        structured_data:
          type: array
          items:
            $ref: '#/components/schemas/StructuredData'
          description: Extracted structured data blocks
        text_direction:
          $ref: '#/components/schemas/TextDirection'
          description: Document text direction from `dir` attribute
        title:
          type:
            - string
            - "null"
          description: Document title from `<title>` tag
        twitter_card:
          type: object
          description: |-
            Twitter Card metadata (twitter:* properties)
            Keys like "card", "site", "creator", "title", "description", "image", etc.
          additionalProperties:
            type: string
          propertyNames:
            type: string
    ImageDimensions:
      type: object
      required:
        - width
        - height
      properties:
        height:
          type: integer
        width:
          type: integer
    ImageDpi:
      type: object
      required:
        - horizontal
        - vertical
      properties:
        horizontal:
          type: number
        vertical:
          type: number
    ImageExtractionConfig:
      type: object
      description: Image extraction and processing configuration.
      properties:
        auto_adjust_dpi:
          type:
            - boolean
            - "null"
          description: Auto-adjust DPI based on content. Defaults to true when omitted.
        extract_images:
          type:
            - boolean
            - "null"
          description: Extract images from documents. Defaults to true when omitted.
        inject_placeholders:
          type:
            - boolean
            - "null"
          description: Inject image reference placeholders in markdown output. Defaults to true when omitted.
        max_dpi:
          type:
            - integer
            - "null"
          format: int32
          description: Maximum DPI threshold. Defaults to 600 when omitted.
        max_image_dimension:
          type:
            - integer
            - "null"
          format: int32
          description: Maximum image dimension (width or height). Defaults to 4096 when omitted.
        min_dpi:
          type:
            - integer
            - "null"
          format: int32
          description: Minimum DPI threshold. Defaults to 72 when omitted.
        target_dpi:
          type:
            - integer
            - "null"
          format: int32
          description: Target DPI for image normalization. Defaults to 300 when omitted.
      additionalProperties: false
    ImageKind:
      type: string
      description: Heuristic classification of what an image likely depicts.
      enum:
        - photograph
        - diagram
        - chart
        - drawing
        - text_block
        - decoration
        - logo
        - icon
        - tile_fragment
        - mask
        - page_raster
        - unknown
    ImageMetadata:
      type: object
      description: |-
        Image metadata extracted from image files.

        Includes dimensions, format, and EXIF data.
      required:
        - width
        - height
        - format
        - exif
        - format_type
      properties:
        exif:
          type: object
          description: EXIF metadata tags
          additionalProperties:
            type: string
          propertyNames:
            type: string
        format:
          type: string
          description: Image format (e.g., "PNG", "JPEG", "TIFF")
        format_type:
          type: string
          enum:
            - image
        height:
          type: integer
          format: int32
          description: Image height in pixels
          minimum: 0
        width:
          type: integer
          format: int32
          description: Image width in pixels
          minimum: 0
    ImageMetadataType:
      type: object
      description: Image element metadata.
      required:
        - src
        - image_type
        - attributes
      properties:
        alt:
          type:
            - string
            - "null"
          description: Alternative text from alt attribute
        attributes:
          type: array
          items:
            $ref: '#/components/schemas/KeyValueAttribute'
          description: Additional attributes as key-value pairs.
        dimensions:
          oneOf:
            - $ref: '#/components/schemas/ImageDimensions'
              description: Image dimensions if available.
            - type: "null"
        image_type:
          $ref: '#/components/schemas/ImageType'
          description: Image type classification
        src:
          type: string
          description: Image source (URL, data URI, or SVG content)
        title:
          type:
            - string
            - "null"
          description: Title attribute
    ImagePreprocessingConfig:
      type: object
      description: |-
        Image preprocessing configuration for OCR.

        These settings control how images are preprocessed before OCR to improve
        text recognition quality. Different preprocessing strategies work better
        for different document types.
      properties:
        auto_rotate:
          type: boolean
          description: Auto-detect and correct image rotation.
          default: false
        binarization_method:
          type: string
          description: |-
            Binarization method: "none" (alias "off"), "otsu", "sauvola", or "adaptive".
            `deskew` must be `false` when this is `none` or `off`.
          default: otsu
        contrast_enhance:
          type: boolean
          description: |-
            Enhance contrast for better text visibility. With `none` or `off` binarization, this
            applies background normalization and sharpening while preserving grayscale pixels.
          default: false
        denoise:
          type: boolean
          description: Remove noise from the image.
          default: false
        deskew:
          type: boolean
          description: Correct skew (tilted images). Must be `false` with `none` or `off` binarization.
          default: true
        invert_colors:
          type: boolean
          description: Invert colors (white text on black → black on white).
          default: false
        normalize_shaded_rows:
          type: boolean
          description: |-
            Normalize shaded table rows (e.g. a subtotal row on a light or dark fill) before
            binarization, so each shaded band is stretched to its own dark-text-on-white
            polarity instead of being lost to a single whole-page threshold (GH#1785).

            This is a per-band step, not a replacement for `binarization_method`: no single
            whole-page method recovers every fill color, and the per-band step itself can
            regress a row style it does not fully model (e.g. a mid-grey fill with white
            text), so it defaults to `false` rather than being enabled unconditionally.

            Has no effect when `binarization_method` is `"none"` or `"off"` **and** `contrast_enhance`
            is `true`: that path re-reads the original image to apply background normalization and
            sharpening, discarding the per-band result. A `WARN` is emitted when the option is
            requested in that combination (GH#1837).
          default: false
        target_dpi:
          type: integer
          format: int32
          description: |-
            Target DPI for the image (300 is standard, 600 for small text).

            For a PDF page, this resamples the already-rendered raster; it does not make the page
            re-render at a higher native resolution. A value above what
            `image::preprocessing::calculate_target_dpi`'s memory/dimension clamp allows is
            silently capped (GH#1786: 400 and 600 both clamped to the same ~372 on a Letter page
            and produced byte-identical output), so upscaling interpolated pixels this way adds no
            detail. To change the actual PDF render resolution, set `images.target_dpi` on
            `ExtractionConfig` instead
            (`image::dpi::effective_pdf_render_dpi`).
          default: 300
      additionalProperties: false
    ImagePreprocessingMetadata:
      type: object
      required:
        - original_dimensions
        - original_dpi
        - target_dpi
        - scale_factor
        - auto_adjusted
        - final_dpi
        - resample_method
        - dimension_clamped
        - skipped_resize
      properties:
        auto_adjusted:
          type: boolean
          description: Whether DPI was auto-adjusted based on content
        calculated_dpi:
          type:
            - integer
            - "null"
          format: int32
          description: Calculated optimal DPI (if auto_adjust_dpi enabled)
        dimension_clamped:
          type: boolean
          description: Whether dimensions were clamped to max_image_dimension
        final_dpi:
          type: integer
          format: int32
          description: Final DPI after processing
        new_dimensions:
          oneOf:
            - $ref: '#/components/schemas/PixelDimensions'
              description: New dimensions after resizing (if resized).
            - type: "null"
        original_dimensions:
          $ref: '#/components/schemas/PixelDimensions'
          description: Original image dimensions in pixels.
        original_dpi:
          $ref: '#/components/schemas/ImageDpi'
          description: Original image resolution.
        resample_method:
          type: string
          description: Resampling algorithm used ("LANCZOS3", "CATMULLROM", etc.)
        resize_error:
          type:
            - string
            - "null"
          description: Error message if resize failed
        scale_factor:
          type: number
          format: double
          description: Scaling factor applied to the image
        skipped_resize:
          type: boolean
          description: Whether resize was skipped (dimensions already optimal)
        target_dpi:
          type: integer
          format: int32
          description: Target DPI from configuration
    ImageType:
      type: string
      description: Image type classification.
      enum:
        - data-uri
        - inline-svg
        - external
        - relative
    IncludeMask:
      type: object
      description: Mask of optional response fields.
      properties:
        content:
          type: boolean
          description: Include chunk content text. Default true.
        document:
          type: boolean
          description: Include parent document summary. Default false.
        score_breakdown:
          type: boolean
          description: Include primary-stage + rerank score breakdown. Default false.
      additionalProperties: false
    InlineElement:
      type: object
      description: |-
        Inline element within a block.

        Represents text with formatting, links, images, etc.
      required:
        - element_type
        - content
      properties:
        attributes:
          $ref: '#/components/schemas/Attributes'
          description: Element attributes
        content:
          type: string
          description: Text content
        element_type:
          $ref: '#/components/schemas/InlineType'
          description: Type of inline element
        metadata:
          type:
            - object
            - "null"
          description: Additional metadata (e.g., href for links, src/alt for images)
          additionalProperties:
            type: string
          propertyNames:
            type: string
    InlineType:
      type: string
      description: Types of inline elements in Djot.
      enum:
        - text
        - strong
        - emphasis
        - highlight
        - subscript
        - superscript
        - insert
        - delete
        - code
        - link
        - image
        - span
        - math
        - raw_inline
        - footnote_ref
        - symbol
    IntegrationInput:
      type: object
      description: |-
        Reference to a document living in a connected integration (e.g. Google Drive).

        No bytes are carried; the receiving service resolves them at fetch time
        through the integration's source connector, scoped to the caller's project.
      required:
        - integration_id
        - resource_id
      properties:
        integration_id:
          type: string
          format: uuid
          description: The `integrations.id` (hyphenated UUID) the document is fetched through.
        resource_id:
          type: string
          format: uuid
          description: Connector-specific document identifier (e.g. a Google Drive file id).
      additionalProperties: false
    JatsMetadata:
      type: object
      description: JATS (Journal Article Tag Suite) metadata.
      required:
        - format_type
      properties:
        contributor_roles:
          type: array
          items:
            $ref: '#/components/schemas/ContributorRole'
          description: Authors and contributors with their stated roles.
        copyright:
          type:
            - string
            - "null"
          description: Copyright statement from the article's `<permissions>` element.
        format_type:
          type: string
          enum:
            - jats
        history_dates:
          type: object
          description: Publication history dates keyed by event type (e.g. `"received"`, `"accepted"`).
          additionalProperties:
            type: string
          propertyNames:
            type: string
        license:
          type:
            - string
            - "null"
          description: Open-access license URI from the article's `<license>` element.
    JobResult:
      type: object
      description: |-
        The result envelope `GET /v1/jobs/{id}/result` serves on both tiers.

        `results` holds the extracted document(s) produced by this job, in
        discovery order: a one-element vector for the common single-document case.
        The two tiers model multi-document splits differently, and this DTO carries
        both. Pro inlines every document in `results`. Enterprise runs one document
        per job, spawns child jobs for a split, and lists their IDs in
        `child_job_ids`, each retrievable on this route.

        Each tier stores the result in its own layout (Pro: an object per job;
        Enterprise: a NATS KV envelope with a 12 hour TTL, offloaded to object
        storage above a size threshold). Both translate to this envelope at the
        HTTP boundary, and both answer 409 with `JobResult.not_ready_message`
        while the job is not in a terminal successful state.
      required:
        - job_id
        - status
        - cached
      properties:
        cached:
          type: boolean
          description: |-
            This job's recorded extraction-cache hit, independent of child jobs and billing.
            False means no recorded hit; a pending job has not necessarily attempted extraction.
        child_job_ids:
          type: array
          items:
            type: string
          description: |-
            Child job IDs when this job was split into multiple jobs (Enterprise
            boundary splitting). Each is retrievable via `GET /v1/jobs/{id}`. Empty
            when the job produced no child jobs (always empty on Pro).
        completed_at:
          type:
            - string
            - "null"
          description: ISO 8601 completion timestamp, when available.
        errors:
          type: array
          items:
            $ref: '#/components/schemas/JobResultError'
          description: Non-fatal per-document errors.
        job_id:
          type: string
          format: uuid
          description: The job this result belongs to.
        results:
          type: array
          items:
            $ref: '#/components/schemas/JobResultDocument'
          description: |-
            Extracted documents, present once the job reaches a terminal
            successful state (`completed` or `partial_success`).

            Opaque JSON on purpose: each document is `xberg::types::ExtractedDocument`
            plus the keys the structured pipeline merges in (`structured_output`,
            `structured_output_flat`). Passing the stored document through untouched
            is what keeps those keys on the wire; a typed round trip through the
            engine's document type dropped every key that type does not declare.
        status:
          $ref: '#/components/schemas/JobStatus'
          description: Job status at the time the result was read.
        webhook_delivery:
          $ref: '#/components/schemas/WebhookDelivery'
          description: Inline webhook delivery state, when this job configured one.
    JobResultCounts:
      type: object
      description: Structural counts for a `JobResultDocument`, always present.
      required:
        - pages
        - tables
        - images
      properties:
        images:
          type: integer
          format: int32
          description: Number of images extracted.
          minimum: 0
        pages:
          type: integer
          format: int32
          description: Number of pages. `0` for formats that are not page-addressable.
          minimum: 0
        tables:
          type: integer
          format: int32
          description: Number of tables extracted.
          minimum: 0
    JobResultDocument:
      type: object
      description: |-
        One extracted document inside `JobResult.results`.

        A **projection** of the engine's `xberg::types::ExtractedDocument`, not a
        separate wire type: the service serializes the engine type itself, and this
        schema names the subset of its fields the API surface documents. Additional
        properties are permitted and do occur — per-page content, OCR elements,
        document structure, archive children and extracted URIs are all carried by
        the payload without appearing here, because registering the engine type
        would pull its entire nested schema graph into the spec.
      required:
        - content
        - mime_type
        - metadata
        - counts
        - tables
      properties:
        chunks:
          type:
            - array
            - "null"
          items: {}
          description: |-
            Text chunks, present only when chunking was requested. Opaque here; the
            engine type is `Chunk`.
        content:
          type: string
          description: |-
            Plain-text (or Markdown, per the requested output format) rendering of
            the document.
        counts:
          $ref: '#/components/schemas/JobResultCounts'
          description: |-
            Structural counts, always present: `{"pages": n, "tables": n,
            "images": n}`. `pages` is `0` for formats that are not page-addressable.
        detected_languages:
          type:
            - array
            - "null"
          items:
            type: string
          description: |-
            ISO language codes detected in the content, when detection ran and
            something met its confidence floor.
        extraction_method:
          type:
            - string
            - "null"
          description: |-
            How the text was produced: `native`, `ocr`, or `mixed`. Absent when the
            extractor cannot distinguish them — a client must not read absence as
            "no OCR".
        metadata:
          type: object
          description: |-
            Document-level metadata (title, authors, dates, page structure,
            format-specific fields). Opaque here; the engine type is `Metadata`.
          additionalProperties: {}
          propertyNames:
            type: string
        mime_type:
          type: string
          description: MIME type the engine resolved for the source document.
        processing_warnings:
          type:
            - array
            - "null"
          items:
            $ref: '#/components/schemas/ProcessingWarning'
          description: |-
            Non-fatal warnings from the extraction pipeline — the signal that an
            otherwise successful job produced degraded output. Absent when the
            document raised none.
        quality_score:
          type:
            - number
            - "null"
          format: double
          description: |-
            Cleanliness/readability score in `[0.0, 1.0]` for the text that was
            retained. Not a completeness score: read `processing_warnings` to learn
            what was dropped.
        structured_output:
          type: object
          description: |-
            LLM-extracted JSON matching the requested schema, present only when the
            job supplied one (`options.preset`, an explicit
            `structured_extraction.schema`, or `options.saved_preset_id`, which
            both tiers apply).
        tables:
          type: array
          items: {}
          description: |-
            Tables extracted from the document. Opaque here; the engine type is
            `Table`.
      additionalProperties: true
    JobResultError:
      type: object
      description: |-
        A non-fatal error attached to one document within a `JobResult`.

        Used when a job processed multiple documents (e.g. via `split_documents`)
        and some, but not all, failed. Fields mirror `xberg`'s per-item error shape
        so Pro's stored errors translate without loss; Enterprise leaves the fields
        it does not populate unset.
      required:
        - error_type
        - message
      properties:
        code:
          type:
            - integer
            - "null"
          format: int64
          description: Numeric error code, when the producing engine assigns one.
        error_type:
          type: string
          description: Machine-readable error kind (e.g. `"unsupported_mime_type"`).
        index:
          type:
            - integer
            - "null"
          format: int64
          description: |-
            Zero-based index of the input document this error applies to, used to
            correlate with `results` in a multi-document job. Unset when the error
            is not attributable to a specific document.
        message:
          type: string
          description: Human-readable error message.
        source:
          type:
            - string
            - "null"
          description: Origin of the error (e.g. the failing stage or component).
    JobStatus:
      type: string
      description: |-
        Lifecycle status of an extraction job.

        Mirrors `domain::jobs::DomainJobStatus` and the Enterprise-local
        `services/api::rest::jobs::JobStatus` enum. Defined independently here
        rather than depending on `crates/domain`: `domain` pulls in the database,
        storage, and NATS stacks, which would be an inappropriate dependency
        weight for a thin wire-types crate.

        Wire form is `snake_case` (`"completed"`, `"partial_success"`), matching
        Enterprise's current serialization. **Pro migration note:** Pro currently
        emits UPPERCASE status strings, so adopting this enum lower-cases them — a
        breaking wire change for existing Pro clients (see ADR-0066).
      enum:
        - awaiting_upload
        - pending
        - processing
        - completed
        - partial_success
        - failed
        - cancelled
    JobSummary:
      type: object
      description: Summary of a job, as returned by `GET /v1/jobs` (list).
      required:
        - job_type
        - id
        - filename
        - mime_type
        - status
        - created_at
      properties:
        completed_at:
          type:
            - string
            - "null"
          description: |-
            ISO 8601 completion timestamp, present once the job reaches a
            terminal state.
        created_at:
          type: string
          format: date-time
          description: ISO 8601 creation timestamp.
        filename:
          type: string
          description: Original filename for an extraction, or the primary seed URL for a crawl.
        id:
          type: string
          format: uuid
          description: Unique job identifier.
        job_type:
          $ref: '#/components/schemas/JobSummaryType'
          description: Kind of detail view represented by this row.
        mime_type:
          type: string
          description: MIME type of the submitted document, or `crawl` for a crawl parent.
        parent_crawl_job_id:
          type:
            - string
            - "null"
          format: uuid
          description: The crawl job that spawned this job as a child, when any.
        source_url:
          type:
            - string
            - "null"
          description: |-
            The crawled page URL this document came from, when the job is a
            crawl child (#1961). Absent for a direct upload.
        status:
          $ref: '#/components/schemas/JobStatus'
          description: Current job status.
        webhook_delivery:
          $ref: '#/components/schemas/WebhookDelivery'
          description: Inline webhook delivery state, when this job configured one.
    JobSummaryType:
      type: string
      description: Kind of job represented by a `JobSummary`.
      enum:
        - extraction
        - crawl
    KeyValueAttribute:
      type: object
      required:
        - key
        - value
      properties:
        key:
          type: string
        value:
          type: string
    Keyword:
      type: object
      description: Extracted keyword with metadata.
      required:
        - text
        - score
        - algorithm
      properties:
        algorithm:
          $ref: '#/components/schemas/KeywordAlgorithm'
          description: Algorithm that extracted this keyword.
        positions:
          type:
            - array
            - "null"
          items:
            type: integer
            minimum: 0
          description: Optional positions where keyword appears in text (character offsets).
        score:
          type: number
          format: float
          description: Relevance score (higher is better, algorithm-specific range).
        text:
          type: string
          description: The keyword text.
    KeywordAlgorithm:
      type: string
      description: Keyword algorithm selection.
      enum:
        - yake
        - rake
    KeywordConfig:
      type: object
      description: Keyword extraction configuration.
      properties:
        algorithm:
          $ref: '#/components/schemas/KeywordAlgorithm'
          description: Algorithm to use for extraction.
        language:
          type:
            - string
            - "null"
          description: |-
            Language code for stopword filtering (e.g., "en", "de", "fr").

            If None, no stopword filtering is applied.
        max_keywords:
          type: integer
          description: 'Maximum number of keywords to extract (default: 10).'
          minimum: 0
        min_score:
          type: number
          format: float
          description: |-
            Minimum score threshold (0.0-1.0, default: 0.0).

            Keywords with scores below this threshold are filtered out.
            Note: Score ranges differ between algorithms.
        ngram_range:
          $ref: '#/components/schemas/NgramRange'
          description: |-
            N-gram range for keyword extraction (min, max).

            (1, 1) = unigrams only
            (1, 2) = unigrams and bigrams
            (1, 3) = unigrams, bigrams, and trigrams (default)
        rake_params:
          $ref: '#/components/schemas/RakeParams'
          description: RAKE-specific tuning parameters.
        yake_params:
          $ref: '#/components/schemas/YakeParams'
          description: YAKE-specific tuning parameters.
      additionalProperties: false
    LanguageConfidence:
      type: object
      description: |-
        Structured per-language detection result: confidence, document share, and script —
        the information the ISO-code-only `detected_languages` list cannot convey (#261).

        Populated by the language-detection processor alongside `detected_languages`, with one
        entry per language, in the same order as `detected_languages`.
      required:
        - language
        - confidence
        - proportion
        - script
        - reliable
      properties:
        confidence:
          type: number
          format: double
          description: |-
            Confidence for this language, in `[0.0, 1.0]`.

            In single-language mode this is whatlang's `Info::confidence()` for the whole
            document. In multi-language mode this is the average whatlang confidence across
            the document's 200-character chunks that were classified as this language.
        language:
          type: string
          description: ISO 639-3 language code, matching the corresponding entry in `detected_languages`.
        proportion:
          type: number
          format: double
          description: |-
            Share of the document's analyzed content classified as this language, in `[0.0, 1.0]`.

            In single-language mode this is always `1.0`. In multi-language mode this is the
            fraction of 200-character chunks classified as this language (chunks that did not
            meet `min_confidence` for any language are excluded from the count but still count
            toward the denominator).
        reliable:
          type: boolean
          description: |-
            Whether this detection is considered reliable.

            In single-language mode this is whatlang's own `Info::is_reliable()` (confidence
            above whatlang's internal 0.9 threshold). In multi-language mode this is the
            chunk-averaged `confidence` above that same 0.9 threshold, since whatlang's
            `is_reliable()` only applies to a single detection.
        script:
          type: string
          description: Writing system whatlang detected for this language (e.g. `"Latin"`, `"Cyrillic"`).
    LanguageDetectionConfig:
      type: object
      description: Language detection configuration.
      properties:
        detect_multiple:
          type:
            - boolean
            - "null"
          description: Detect multiple languages in document
        enabled:
          type:
            - boolean
            - "null"
          description: Enable language detection. Defaults to true when omitted.
        min_confidence:
          type:
            - number
            - "null"
          format: double
          description: Minimum confidence threshold (0.0-1.0). Defaults to 0.8 when omitted.
      additionalProperties: false
    LatencyBreakdown:
      type: object
      description: Latency breakdown (milliseconds) for a single retrieve call.
      required:
        - retrieve_ms
        - total_ms
      properties:
        rerank_ms:
          type:
            - integer
            - "null"
          format: int64
          description: |-
            Reranker call (when active). `None` when rerank was off or
            fell back to primary order.
          minimum: 0
        retrieve_ms:
          type: integer
          format: int64
          description: Primary-stage retrieve (vector / fts / hybrid).
          minimum: 0
        total_ms:
          type: integer
          format: int64
          description: End-to-end including response serialization.
          minimum: 0
    LatestDocumentResponse:
      type: object
      description: |-
        Latest version of a document, with its extraction result when one is
        available.

        Every field is always present; `result` carries `null` (rather than being
        omitted) when the document has no completed extraction yet or its stored
        result could not be read back.
      required:
        - id
        - document_id
        - version_sequence
        - status
        - filename
        - mime_type
        - created_at
      properties:
        created_at:
          type: string
          format: date-time
          description: When this version was created.
        document_id:
          type: string
          format: uuid
          description: Logical document ID (the path parameter, echoed back).
          example: 550e8400-e29b-41d4-a716-446655440000
        filename:
          type: string
          description: Original filename.
        id:
          type: string
          format: uuid
          description: Job ID of the latest version.
          example: 550e8400-e29b-41d4-a716-446655440000
        mime_type:
          type: string
          description: MIME type of the uploaded file.
        result:
          oneOf:
            - $ref: '#/components/schemas/ExtractedDocument'
              description: |-
                The extraction result, when the stored result could be fetched and
                deserialized; `null` otherwise.
            - type: "null"
        status:
          type: string
          description: Job status of the latest version.
        version_sequence:
          type: integer
          format: int32
          description: Version sequence number (incrementing per document).
    LayoutDetectionConfig:
      type: object
      description: Document layout detection configuration.
      properties:
        apply_heuristics:
          type:
            - boolean
            - "null"
          description: Apply postprocessing heuristics. Defaults to true when omitted.
        confidence_threshold:
          type:
            - number
            - "null"
          format: float
          description: Confidence threshold override
        preset:
          type:
            - string
            - "null"
          description: |-
            Layout presets are unavailable. xberg's `LayoutDetectionConfig` has no
            preset concept, and the wire config has no field to carry one, so a
            value here reached nothing. Omit this field; setting it returns HTTP 400.
            Set `confidence_threshold` and `apply_heuristics` directly instead.
        table_model:
          type:
            - string
            - "null"
          description: |-
            Table structure model: "tatr", "slanet_wired", "slanet_wireless",
            "slanet_plus", "slanet_auto" or "disabled". Engine default when omitted.
        table_overlap_preference:
          type:
            - string
            - "null"
          description: |-
            Which region wins when a table and another block overlap. Engine
            default when omitted.
      additionalProperties: false
    LayoutRegion:
      type: object
      description: |-
        A detected layout region on a page.

        When layout detection is enabled, each page may have layout regions
        identifying different content types (text, pictures, tables, etc.)
        with confidence scores and spatial positions.
      required:
        - class_name
        - confidence
        - bounding_box
        - area_fraction
      properties:
        area_fraction:
          type: number
          format: double
          description: Fraction of the page area covered by this region (0.0 to 1.0).
        bounding_box:
          $ref: '#/components/schemas/BoundingBox'
          description: Bounding box in document coordinate space.
        class_name:
          type: string
          description: Layout class name (e.g. "picture", "table", "text", "section_header").
        confidence:
          type: number
          format: double
          description: Confidence score from the layout detection model (0.0 to 1.0).
    LeaderboardEntry:
      type: object
      description: One entry in an auto-tune run's leaderboard.
      required:
        - config
        - primary_score
        - variance
        - breakdown
        - tied_with_winner
      properties:
        breakdown:
          type: object
          description: |-
            Per-dimension score breakdown (e.g. per-document or per-metric
            scores). The concrete shape is defined in `autotune-core`; here it
            is opaque JSON.
        config:
          type: object
          description: |-
            The extraction configuration trialed for this entry. The concrete
            shape is defined in `autotune-core`; here it is opaque JSON.
        primary_score:
          type: number
          format: double
          description: Primary aggregate score for this configuration.
        tied_with_winner:
          type: boolean
          description: |-
            Whether this entry is statistically tied with the winning entry
            within the configured tolerance band.
        variance:
          type: number
          format: double
          description: |-
            Variance of the score across documents/trials, surfaced so callers
            can judge score stability rather than trusting a single number.
    LinkMetadata:
      type: object
      description: Link element metadata.
      required:
        - href
        - text
        - link_type
        - rel
        - attributes
      properties:
        attributes:
          type: array
          items:
            $ref: '#/components/schemas/KeyValueAttribute'
          description: Additional attributes as key-value pairs.
        href:
          type: string
          description: The href URL value
        link_type:
          $ref: '#/components/schemas/LinkType'
          description: Link type classification
        rel:
          type: array
          items:
            type: string
          description: Rel attribute values
        text:
          type: string
          description: Link text content (normalized)
        title:
          type:
            - string
            - "null"
          description: Optional title attribute
    LinkType:
      type: string
      description: Link type classification.
      enum:
        - anchor
        - internal
        - external
        - email
        - phone
        - other
    ListAuditEntriesResponse:
      type: object
      description: Response for the customer-facing audit read endpoint (`GET /v1/audit`).
      required:
        - entries
        - total
        - limit
        - offset
      properties:
        entries:
          type: array
          items:
            $ref: '#/components/schemas/AuditEntry'
          description: Audit entries in the current page, most recent first.
        limit:
          type: integer
          format: int64
          description: Page size used.
        offset:
          type: integer
          format: int64
          description: Offset used for pagination.
        total:
          type: integer
          format: int64
          description: Total number of audit entries matching the query.
    ListAutoTuneJobsResponse:
      type: object
      description: Response from listing auto-tune jobs.
      required:
        - jobs
        - total
      properties:
        jobs:
          type: array
          items:
            $ref: '#/components/schemas/AutoTuneJobSummary'
          description: Auto-tune jobs for the current project (newest first).
        total:
          type: integer
          format: int64
          description: Total count of auto-tune jobs in the project.
    ListCollectionsResponse:
      type: object
      description: Response for listing collections (paginated)
      required:
        - collections
        - total
        - limit
        - offset
      properties:
        collections:
          type: array
          items:
            $ref: '#/components/schemas/CollectionResponse'
          description: List of collections
        limit:
          type: integer
          format: int32
          description: Maximum results per page
        offset:
          type: integer
          format: int32
          description: Number of results skipped
        total:
          type: integer
          format: int32
          description: Total number of collections for this project
    ListExtractionEventsResponse:
      type: object
      description: Response from listing extraction events.
      required:
        - events
        - total
        - page
        - limit
      properties:
        events:
          type: array
          items:
            $ref: '#/components/schemas/ExtractionEventSummary'
          description: List of extraction events for the current project.
        limit:
          type: integer
          format: int64
          description: Page size used.
        page:
          type: integer
          format: int64
          description: Current page (0-indexed).
        total:
          type: integer
          format: int64
          description: Total count of events.
    ListJobsResponse:
      type: object
      description: Response for `GET /v1/jobs` (paginated job list).
      required:
        - jobs
        - total
        - limit
        - offset
      properties:
        jobs:
          type: array
          items:
            $ref: '#/components/schemas/JobSummary'
          description: Jobs in the current page, most recent first.
        limit:
          type: integer
          format: int64
          description: Page size used.
        offset:
          type: integer
          format: int64
          description: Offset used for pagination.
        total:
          type: integer
          format: int64
          description: Total number of jobs for the project.
    ListManagedEmbeddingPresetsResponse:
      type: object
      description: Bounded list of managed embedding presets shipped by this server build.
      required:
        - presets
      properties:
        presets:
          type: array
          items:
            $ref: '#/components/schemas/ManagedEmbeddingPreset'
          description: Available presets, in the canonical backend order.
          maxItems: 32
    ListSavedPresetsResponse:
      type: object
      description: Response from listing saved presets.
      required:
        - presets
        - total
        - page
        - limit
      properties:
        limit:
          type: integer
          format: int64
          description: Page size used.
        page:
          type: integer
          format: int64
          description: Current page (0-indexed).
        presets:
          type: array
          items:
            $ref: '#/components/schemas/SavedPresetSummary'
          description: List of saved presets for the current project.
        total:
          type: integer
          format: int64
          description: Total count of presets in the project.
    ListTuningProfilesResponse:
      type: object
      description: Response from listing tuning profiles.
      required:
        - profiles
        - total
      properties:
        profiles:
          type: array
          items:
            $ref: '#/components/schemas/TuningProfileSummary'
          description: Tuning profiles for the current project.
        total:
          type: integer
          format: int64
          description: Total count of tuning profiles in the project.
    ListVersionsQuery:
      type: object
      description: Query parameters for document versions list.
      properties:
        limit:
          type: integer
          format: int32
          description: Maximum number of versions to return (default 50, max 200).
        offset:
          type: integer
          format: int32
          description: Number of versions to skip (default 0, clamped to a maximum of 1,000,000).
      additionalProperties: false
    ListWebhookDeliveriesResponse:
      type: object
      description: Response for the paginated webhook delivery-history endpoint.
      required:
        - deliveries
        - total
        - limit
        - offset
      properties:
        deliveries:
          type: array
          items:
            $ref: '#/components/schemas/WebhookDeliveryResponse'
          description: Delivery attempts in the current page, newest first.
        limit:
          type: integer
          format: int64
          description: Page size used.
        offset:
          type: integer
          format: int64
          description: Offset used for pagination.
        total:
          type: integer
          format: int64
          description: Total number of delivery attempts recorded for the webhook.
    LlmConfig:
      type: object
      description: LLM provider configuration routed through liter-llm.
      required:
        - model
        - api_key
      properties:
        api_key:
          type: string
          description: |-
            API key for the provider. Mutually exclusive with `use_managed = true`.
            Treated as a secret — never logged. Omit to leave it unset; explicit
            `null` is not a distinct value from omission and is rejected.
        base_url:
          type:
            - string
            - "null"
          description: Override the provider base URL.
        max_retries:
          type:
            - integer
            - "null"
          format: int32
          description: Max retry attempts.
          minimum: 0
        max_tokens:
          type:
            - integer
            - "null"
          format: int64
          description: Maximum tokens to generate.
          minimum: 0
        model:
          type: string
          description: liter-llm routing identifier, e.g. "openai/gpt-4o-mini".
          pattern: ^(openai|anthropic|gemini|groq)/[A-Za-z0-9._:/-]+$
        temperature:
          type:
            - number
            - "null"
          format: double
          description: Sampling temperature.
        timeout_secs:
          type:
            - integer
            - "null"
          format: int64
          description: Request timeout in seconds.
          minimum: 0
        use_managed:
          type: boolean
          description: |-
            Managed provider keys are not available. The worker holds no key to
            substitute, so a request that sets this is rejected with a 400.
      additionalProperties: false
    LlmUsage:
      type: object
      description: |-
        Token usage and cost data for a single LLM call made during extraction.

        Populated when VLM OCR, structured extraction, or LLM-based embeddings
        are used. Multiple entries may be present when multiple LLM calls occur
        within one extraction (e.g. VLM OCR + structured extraction).
      required:
        - model
        - source
      properties:
        estimated_cost:
          type:
            - number
            - "null"
          format: double
          description: Estimated cost in USD based on the provider's published pricing.
        finish_reason:
          type:
            - string
            - "null"
          description: Why the model stopped generating (e.g. "stop", "length", "content_filter").
        input_tokens:
          type:
            - integer
            - "null"
          format: int64
          description: Number of input/prompt tokens consumed.
          minimum: 0
        model:
          type: string
          description: The LLM model identifier (e.g. "openai/gpt-4o", "anthropic/claude-sonnet-4-20250514").
        output_tokens:
          type:
            - integer
            - "null"
          format: int64
          description: Number of output/completion tokens generated.
          minimum: 0
        source:
          type: string
          description: |-
            The pipeline stage that triggered this LLM call
            (e.g. "vlm_ocr", "structured_extraction", "embeddings").
        total_tokens:
          type:
            - integer
            - "null"
          format: int64
          description: Total tokens (input + output).
          minimum: 0
    ManagedEmbeddingPreset:
      type: object
      description: One server-managed embedding preset available to RAG collections.
      required:
        - name
        - embedding_source
        - embedding_dim
        - description
        - is_default
      properties:
        description:
          type: string
          description: Human-readable intended use.
        embedding_dim:
          type: integer
          format: int32
          description: Vector width emitted by the preset.
          minimum: 0
        embedding_source:
          type: string
          description: Canonical value accepted by collection create and migration requests.
        is_default:
          type: boolean
          description: Whether this is the default managed preset for omitted sources.
        name:
          type: string
          description: Stable preset name.
    MarkdownCodeBlock:
      type: object
      required:
        - language
        - code
      properties:
        code:
          type: string
        language:
          type: string
    MarkdownLink:
      type: object
      required:
        - text
        - url
      properties:
        text:
          type: string
        url:
          type: string
    Metadata:
      type: object
      description: |-
        Extraction result metadata.

        Contains common fields applicable to all formats, format-specific metadata
        via a discriminated union, and additional custom fields from postprocessors.
      properties:
        abstract_text:
          type:
            - string
            - "null"
          description: Abstract or summary text (from frontmatter).
        additional:
          type: object
          description: |-
            Additional custom fields from postprocessors.

            Serialized as a nested `"additional"` object (not flattened at root level).
            Uses `Cow<'static, str>` keys so static string keys avoid allocation.
          additionalProperties: {}
          propertyNames:
            type: string
        authors:
          type:
            - array
            - "null"
          items:
            type: string
          description: Primary author(s) - always Vec for consistency
        category:
          type:
            - string
            - "null"
          description: Document category (from frontmatter or classification).
        created_at:
          type:
            - string
            - "null"
          description: Creation timestamp (ISO 8601 format)
        created_by:
          type:
            - string
            - "null"
          description: User who created the document
        document_version:
          type:
            - string
            - "null"
          description: Document version string (from frontmatter).
        error:
          $ref: '#/components/schemas/ErrorMetadata'
          description: Error metadata (for batch operations)
        extraction_duration_ms:
          type:
            - integer
            - "null"
          format: int64
          description: |-
            Extraction duration in milliseconds (for benchmarking).

            This field is populated by batch extraction to provide per-file timing
            information. It's `None` for single-file extraction (which uses external timing).
          minimum: 0
        format:
          $ref: '#/components/schemas/FormatMetadata'
          description: |-
            Format-specific metadata (discriminated union)

            Contains detailed metadata specific to the document format.
            Serialized as a nested `"format"` object with a `format_type` discriminator field.
        image_preprocessing:
          $ref: '#/components/schemas/ImagePreprocessingMetadata'
          description: Image preprocessing metadata (when OCR preprocessing was applied)
        json_schema:
          description: JSON schema (for structured data extraction)
        keywords:
          type:
            - array
            - "null"
          items:
            type: string
          description: Keywords/tags - always Vec for consistency
        language:
          type:
            - string
            - "null"
          description: Primary language (ISO 639 code)
        modified_at:
          type:
            - string
            - "null"
          description: Last modification timestamp (ISO 8601 format)
        modified_by:
          type:
            - string
            - "null"
          description: User who last modified the document
        ocr_used:
          type: boolean
          description: |-
            Whether OCR was used during extraction.

            Set to `true` whenever the extraction pipeline ran an OCR backend
            (Tesseract, PaddleOCR, VLM, etc.) and used that output as the primary
            or fallback text. `false` means native text extraction was used exclusively.
        output_format:
          type:
            - string
            - "null"
          description: |-
            Output format identifier (e.g., "markdown", "html", "text").

            Set by the output format pipeline stage when format conversion is applied.
            Previously stored in `metadata.additional["output_format"]`.
        pages:
          $ref: '#/components/schemas/PageStructure'
          description: Page/slide/sheet structure with boundaries
        subject:
          type:
            - string
            - "null"
          description: Document subject or description
        tags:
          type:
            - array
            - "null"
          items:
            type: string
          description: Document tags (from frontmatter).
        title:
          type:
            - string
            - "null"
          description: Document title
    MigrateEmbeddingsRequest:
      type: object
      description: Request to migrate a collection's embedding source.
      required:
        - target_embedding_source
        - target_embedding_version
      properties:
        target_embedding_source:
          type: string
          description: |-
            Target embedding source, for example `managed_local`,
            `managed_local:quality` or `provider:<name>`.

            The legacy `preset:<name>` spelling is still accepted. It is not what
            comes back: see `MigrateEmbeddingsResponse.to_source`.
          minLength: 1
        target_embedding_version:
          type: integer
          format: int32
          description: Target embedding version (must be > collection's current embedding_version).
          maximum: 2147483647
          minimum: 1
        webhook:
          $ref: '#/components/schemas/WebhookConfig'
          description: Optional webhook for migration completion/failure delivery.
      additionalProperties: false
    MigrateEmbeddingsResponse:
      type: object
      description: Response to a migrate embeddings request.
      required:
        - job_id
        - collection_id
        - from_source
        - to_source
        - from_version
        - to_version
        - status
        - poll
      properties:
        collection_id:
          type: string
          format: uuid
          description: Collection being migrated.
        from_source:
          type: string
          description: |-
            Current embedding source, as stored, which is the spelling the create
            request used rather than a canonical rendering.
        from_version:
          type: integer
          format: int32
          description: Current embedding version.
          minimum: 0
        job_id:
          type: string
          description: Job ID for polling progress (ULID).
        poll:
          type: string
          description: 'Poll endpoint: `/v1/rag/collections/{name}/migrate-embeddings/{job_id}`.'
        status:
          $ref: '#/components/schemas/MigrateStatus'
          description: Current migration status.
        to_source:
          type: string
          description: |-
            Target embedding source, in its canonical spelling.

            This is the parsed rendering, not the string the request carried, so a
            request sending the legacy `preset:balanced` gets `managed_local` back.
            The same value reaches the migration message and, on completion,
            `vector_collections.embedding_source`, which is what keeps that column
            on one spelling. Match on it by parsing rather than by string equality.
        to_version:
          type: integer
          format: int32
          description: Target embedding version.
          minimum: 0
    MigrateProgress:
      type: object
      description: Detailed progress of an ongoing or completed migration.
      required:
        - documents_total
        - documents_dual_written
        - current_phase
      properties:
        current_phase:
          type: string
          description: Current phase of migration.
        documents_dual_written:
          type: integer
          format: int64
          description: Documents successfully dual-written to the new version.
          minimum: 0
        documents_total:
          type: integer
          format: int64
          description: Total documents to migrate.
          minimum: 0
    MigrateStatus:
      type: string
      description: Current status of a migration job.
      enum:
        - pending
        - running
        - completed
        - failed
    MigrateStatusResponse:
      type: object
      description: Status response for migration progress polling.
      required:
        - job_id
        - status
      properties:
        error:
          type:
            - string
            - "null"
          description: Error message (only when failed).
        error_code:
          type:
            - string
            - "null"
          description: Machine-readable cause when the failed job has a known classification.
        job_id:
          type: string
          description: Job ID (ULID).
        progress:
          $ref: '#/components/schemas/MigrateProgress'
          description: Progress details (only available when running or completed).
        status:
          $ref: '#/components/schemas/MigrateStatus'
          description: Current status.
    MultiVectorEmbedding:
      type: object
      description: |
        A ColBERT multi-vector embedding: one row per attention-live token.

        `data` is a flat, row-major buffer of length `num_tokens * dim` — row `i`
        (the embedding for token `i`) occupies `data[i*dim .. (i+1)*dim]`. Flat
        storage keeps the type FFI-friendly across binding boundaries; use
        `MultiVectorEmbedding.rows` internally to iterate per-token slices.
      required:
        - num_tokens
        - dim
        - data
      properties:
        data:
          type: array
          items:
            type: number
            format: float
          description: Flat row-major buffer, length `num_tokens * dim`.
        dim:
          type: integer
          format: int32
          description: Dimensionality of each per-token vector.
          minimum: 0
        num_tokens:
          type: integer
          format: int32
          description: |-
            Number of attention-live token rows (padding rows are dropped, not
            zeroed — see token normalization).
          minimum: 0
    NgramRange:
      type: object
      required:
        - min
        - max
      properties:
        max:
          type: integer
          minimum: 1
        min:
          type: integer
          minimum: 1
    NodeContent:
      oneOf:
        - type: object
          description: Document title.
          required:
            - text
            - node_type
          properties:
            node_type:
              type: string
              enum:
                - title
            text:
              type: string
              description: The title text content.
        - type: object
          description: Section heading with level (1-6).
          required:
            - level
            - text
            - node_type
          properties:
            level:
              type: integer
              format: int32
              description: Heading depth (1 = h1, 2 = h2, …, 6 = h6).
              minimum: 0
            node_type:
              type: string
              enum:
                - heading
            text:
              type: string
              description: The heading text content.
        - type: object
          description: Body text paragraph.
          required:
            - text
            - node_type
          properties:
            node_type:
              type: string
              enum:
                - paragraph
            text:
              type: string
              description: The paragraph text content.
        - type: object
          description: List container — children are `ListItem` nodes.
          required:
            - ordered
            - node_type
          properties:
            node_type:
              type: string
              enum:
                - list
            ordered:
              type: boolean
              description: '`true` for ordered (numbered) lists; `false` for unordered (bullet) lists.'
        - type: object
          description: Individual list item.
          required:
            - text
            - node_type
          properties:
            node_type:
              type: string
              enum:
                - list_item
            text:
              type: string
              description: The list item text content.
        - type: object
          description: Table with structured cell grid.
          required:
            - grid
            - node_type
          properties:
            grid:
              $ref: '#/components/schemas/TableGrid'
              description: Structured grid of table cells.
            node_type:
              type: string
              enum:
                - table
        - type: object
          description: Image reference.
          required:
            - node_type
          properties:
            description:
              type:
                - string
                - "null"
              description: Optional alt text or caption describing the image.
            image_index:
              type:
                - integer
                - "null"
              format: int32
              description: Index into the parent `ExtractedDocument::images` list.
              minimum: 0
            node_type:
              type: string
              enum:
                - image
            src:
              type:
                - string
                - "null"
              description: Source URL or path of the image (from `<img src="...">` or `![](src)`).
        - type: object
          description: Code block.
          required:
            - text
            - node_type
          properties:
            language:
              type:
                - string
                - "null"
              description: Programming language identifier (e.g. `"rust"`, `"python"`).
            node_type:
              type: string
              enum:
                - code
            text:
              type: string
              description: The source code text content.
        - type: object
          description: Block quote — container, children carry the quoted content.
          required:
            - node_type
          properties:
            node_type:
              type: string
              enum:
                - quote
        - type: object
          description: Mathematical formula / equation.
          required:
            - text
            - node_type
          properties:
            node_type:
              type: string
              enum:
                - formula
            text:
              type: string
              description: The formula source text (LaTeX or plain mathematical notation).
        - type: object
          description: Footnote reference content.
          required:
            - text
            - node_type
          properties:
            node_type:
              type: string
              enum:
                - footnote
            text:
              type: string
              description: The footnote body text.
        - type: object
          description: |-
            Reviewer/editor comment content (e.g. DOCX comments).

            Distinct from `NodeContent.Footnote` (xberg-io/xberg#300): comments and
            footnotes both reach the internal document via a marker/definition pair, but
            a consumer needs to tell a reviewer comment apart from an authored footnote.
          required:
            - text
            - node_type
          properties:
            node_type:
              type: string
              enum:
                - comment
            text:
              type: string
              description: The comment body text.
        - type: object
          description: |-
            Logical grouping container (section, key-value area).

            `heading_level` + `heading_text` capture the section heading directly
            rather than relying on a first-child positional convention.
          required:
            - node_type
          properties:
            heading_level:
              type:
                - integer
                - "null"
              format: int32
              description: Heading level of the section heading that opened this group (1-6).
              minimum: 0
            heading_text:
              type:
                - string
                - "null"
              description: Text of the section heading that opened this group.
            label:
              type:
                - string
                - "null"
              description: Optional display label for the group (e.g. section name).
            node_type:
              type: string
              enum:
                - group
        - type: object
          description: Page break marker.
          required:
            - node_type
          properties:
            node_type:
              type: string
              enum:
                - page_break
        - type: object
          description: Presentation slide container — children are the slide's content nodes.
          required:
            - number
            - node_type
          properties:
            node_type:
              type: string
              enum:
                - slide
            number:
              type: integer
              format: int32
              description: 1-indexed slide number.
              minimum: 0
            title:
              type:
                - string
                - "null"
              description: Slide title text, if present.
        - type: object
          description: Definition list container — children are `DefinitionItem` nodes.
          required:
            - node_type
          properties:
            node_type:
              type: string
              enum:
                - definition_list
        - type: object
          description: Individual definition list entry with term and definition.
          required:
            - term
            - definition
            - node_type
          properties:
            definition:
              type: string
              description: The definition or description of the term.
            node_type:
              type: string
              enum:
                - definition_item
            term:
              type: string
              description: The term being defined.
        - type: object
          description: Citation or bibliographic reference.
          required:
            - key
            - text
            - node_type
          properties:
            key:
              type: string
              description: Citation key (e.g. BibTeX key or reference ID).
            node_type:
              type: string
              enum:
                - citation
            text:
              type: string
              description: Formatted citation text as it appears in the document.
        - type: object
          description: |-
            Admonition / callout container (note, warning, tip, etc.).

            Children carry the admonition body content.
          required:
            - kind
            - node_type
          properties:
            kind:
              type: string
              description: Kind of admonition (e.g. "note", "warning", "tip", "danger").
            node_type:
              type: string
              enum:
                - admonition
            title:
              type:
                - string
                - "null"
              description: Optional explicit title overriding the default kind label.
        - type: object
          description: |-
            Raw block preserved verbatim from the source format.

            Used for content that cannot be mapped to a semantic node type
            (e.g. JSX in MDX, raw LaTeX in markdown, embedded HTML).
          required:
            - format
            - content
            - node_type
          properties:
            content:
              type: string
              description: Verbatim source content in the specified format.
            format:
              type: string
              description: Source format identifier (e.g. "html", "latex", "jsx").
            node_type:
              type: string
              enum:
                - raw_block
        - type: object
          description: Structured metadata block (email headers, YAML frontmatter, etc.).
          required:
            - entries
            - node_type
          properties:
            entries:
              type: array
              items:
                $ref: '#/components/schemas/KeyValueAttribute'
              description: Key-value pairs extracted from the metadata block.
            node_type:
              type: string
              enum:
                - metadata_block
      description: |-
        Tagged enum for node content. Each variant carries only type-specific data.

        Uses `#[serde(tag = "node_type")]` to avoid "type" keyword collision in
        Go/Java/TypeScript bindings.
    NodeIndex:
      type: integer
      format: int32
      description: |-
        Newtype for node indices into the `DocumentStructure::nodes` array.

        Uses `u32` for cross-platform consistency (WASM is 32-bit) and to avoid
        confusion with page numbers or other `usize` values.
      minimum: 0
    OcrBackendCapabilityInfo:
      type: object
      description: One OCR backend available to this deployment, for capability discovery.
      required:
        - name
        - cost
        - in_default_ladder
      properties:
        cost:
          $ref: '#/components/schemas/OcrBackendCostKind'
          description: Cost classification of running this backend.
        in_default_ladder:
          type: boolean
          description: Whether this backend is included in the deployment's default OCR search ladder.
        name:
          type: string
          description: Registered backend name (e.g. `tesseract`, `paddle-ocr`).
        supported_languages:
          type: array
          items:
            type: string
          description: Known language identifiers advertised by this backend. The list can be non-exhaustive.
        supported_languages_exhaustive:
          type: boolean
          description: Whether `supported_languages` contains every identifier accepted by the backend.
    OcrBackendCostKind:
      type: string
      description: Kind classification for the cost of running an OCR backend.
      enum:
        - local
        - llm_metered
    OcrBoundingGeometry:
      oneOf:
        - type: object
          description: Axis-aligned bounding box (typical for Tesseract output).
          required:
            - left
            - top
            - width
            - height
            - type
          properties:
            height:
              type: integer
              format: int32
              description: Height in pixels
              minimum: 0
            left:
              type: integer
              format: int32
              description: Left x-coordinate in pixels
              minimum: 0
            top:
              type: integer
              format: int32
              description: Top y-coordinate in pixels
              minimum: 0
            type:
              type: string
              enum:
                - rectangle
            width:
              type: integer
              format: int32
              description: Width in pixels
              minimum: 0
        - type: object
          description: |-
            4-point quadrilateral for rotated/skewed text (PaddleOCR).

            Points are in clockwise order starting from top-left:
            `[top_left, top_right, bottom_right, bottom_left]`
          required:
            - points
            - type
          properties:
            points:
              type: array
              items:
                $ref: '#/components/schemas/OcrPoint'
              description: Exactly four corner points in clockwise order.
            type:
              type: string
              enum:
                - quadrilateral
      description: |-
        Bounding geometry for an OCR element.

        Supports both axis-aligned rectangles (from Tesseract) and 4-point quadrilaterals
        (from PaddleOCR and rotated text detection).
    OcrConfidence:
      type: object
      description: |-
        Confidence scores for an OCR element.

        Separates detection confidence (how confident that text exists at this location)
        from recognition confidence (how confident about the actual text content).
      required:
        - recognition
      properties:
        detection:
          type:
            - number
            - "null"
          format: double
          description: |-
            Detection confidence: how confident the OCR engine is that text exists here.

            PaddleOCR provides this as `box_score`, Tesseract doesn't have a direct equivalent.
            Range: 0.0 to 1.0 (or None if not available).
        recognition:
          type: number
          format: double
          description: |-
            Recognition confidence: how confident about the text content.

            Range: 0.0 to 1.0.
    OcrConfig:
      type: object
      description: OCR backend and language configuration.
      properties:
        auto_rotate:
          type:
            - boolean
            - "null"
          description: Automatic page rotation detection
        backend:
          type:
            - string
            - "null"
          description: |-
            OCR backend name. Must be one this deployment has compiled in and
            registered; the accepted set is deployment-dependent and is rejected
            with a 400 when not recognized.
        backend_options:
          type: object
          description: |-
            Per-call options for the named OCR backend, as a JSON object.

            The accepted keys depend on the backend. The candle backends accept a
            `device` (`auto`, `cpu`, `cuda`, `metal`) plus their own keys:
            `candle-deepseek-ocr` takes `model_id` (default
            `deepseek-ai/DeepSeek-OCR`), an optional `model_path` that takes
            precedence when set, `version`, `hf_revision` (required for a
            non-default `model_id`), `cache_dir` and `dtype`;
            `candle-paddleocr-vl` takes `task`, `model_path`, `model_id`,
            `hf_revision` and `cache_dir`; `candle-glm-ocr` takes `task`,
            `layout_mode`, `enable_chart_understanding` and `cache_dir`;
            `candle-trocr` takes `variant`, `hf_revision` and `cache_dir`. An option
            the named backend does not accept is rejected with a 400.
        element_config:
          $ref: '#/components/schemas/OcrElementConfig'
          description: Structured OCR element extraction
        language:
          type:
            - string
            - "null"
          description: Language code (e.g., "eng", "deu", "fra")
        output_format:
          type:
            - string
            - "null"
          description: OCR output format override
        paddle_ocr_settings:
          type: object
          description: PaddleOCR model and inference configuration.
        pipeline:
          $ref: '#/components/schemas/OcrPipelineConfig'
          description: Multi-backend OCR pipeline with quality-based fallback
        psm:
          type:
            - integer
            - "null"
          format: int32
          description: |-
            Tesseract page-segmentation mode (1..=13).

            Present because this service's own auto-tune sweeps it (`Knob::Psm`) and
            returns it inside a winning profile. Without the field the DTO dropped it
            at parse, so a profile this API produced could not be replayed through
            this API. When this synthesizes a Tesseract config, the engine carries
            the top-level language into that config.
        quality_thresholds:
          $ref: '#/components/schemas/OcrQualityThresholds'
          description: Quality thresholds for OCR fallback decisions
        tesseract_config:
          $ref: '#/components/schemas/TesseractConfig'
          description: Tesseract-specific configuration
        vlm_config:
          type: object
          description: VLM (Vision Language Model) configuration
        vlm_fallback:
          $ref: '#/components/schemas/VlmFallbackPolicy'
          description: |-
            Request VLM fallback using the deployment's model.
            An explicit OCR pipeline takes precedence, so it ignores this fallback policy.
        vlm_prompt:
          type:
            - string
            - "null"
          description: Custom Jinja2 prompt template for VLM OCR
      additionalProperties: false
    OcrElement:
      type: object
      description: |-
        A unified OCR element representing detected text with full metadata.

        This is the primary type for structured OCR output, preserving all information
        from both Tesseract and PaddleOCR backends.
      required:
        - text
        - geometry
        - confidence
      properties:
        backend_metadata:
          type: object
          description: Backend-specific metadata that doesn't fit the unified schema.
          additionalProperties: {}
          propertyNames:
            type: string
        confidence:
          $ref: '#/components/schemas/OcrConfidence'
          description: Confidence scores for detection and recognition.
        geometry:
          $ref: '#/components/schemas/OcrBoundingGeometry'
          description: Bounding geometry (rectangle or quadrilateral).
        level:
          $ref: '#/components/schemas/OcrElementLevel'
          description: Hierarchical level (word, line, block, page).
        page_number:
          type: integer
          format: int32
          description: Page number (1-indexed).
          minimum: 0
        parent_id:
          type:
            - string
            - "null"
          description: |-
            Parent element ID for hierarchical relationships.

            When hierarchy output is enabled, this resolves to another emitted element's
            `backend_metadata["element_id"]` value.
        rotation:
          $ref: '#/components/schemas/OcrRotation'
          description: Rotation information (if detected).
        text:
          type: string
          description: The recognized text content.
    OcrElementConfig:
      type: object
      description: Structured OCR element extraction configuration.
      properties:
        build_hierarchy:
          type: boolean
          description: Build parent-child relationships
        include_elements:
          type: boolean
          description: Include OCR elements in result
        min_confidence:
          type:
            - number
            - "null"
          format: double
          description: Minimum recognition confidence (0.0-1.0)
        min_level:
          type:
            - string
            - "null"
          description: 'Minimum hierarchical level: "word", "line", "block", "page"'
      additionalProperties: false
    OcrElementLevel:
      type: string
      description: |-
        Hierarchical level of an OCR element.

        Maps to Tesseract's page segmentation hierarchy and provides
        equivalent semantics for PaddleOCR.
      enum:
        - word
        - line
        - block
        - page
    OcrMetadata:
      type: object
      description: |-
        OCR processing metadata.

        Captures information about OCR processing configuration and results.
      required:
        - language
        - psm
        - output_format
        - table_count
        - format_type
      properties:
        format_type:
          type: string
          enum:
            - ocr
        language:
          type: string
          description: OCR language code(s) used
        output_format:
          type: string
          description: Output format (e.g., "text", "hocr")
        psm:
          type: integer
          format: int32
          description: Tesseract Page Segmentation Mode (PSM)
        table_cols:
          type:
            - integer
            - "null"
          format: int32
          description: Number of columns in the detected table (if a single table was found).
          minimum: 0
        table_count:
          type: integer
          format: int32
          description: Number of tables detected
          minimum: 0
        table_rows:
          type:
            - integer
            - "null"
          format: int32
          description: Number of rows in the detected table (if a single table was found).
          minimum: 0
    OcrPageFailure:
      type: object
      description: |-
        A page whose OCR failed while extraction of the document continued.

        The warning in `ExtractedDocument::processing_warnings` stays available. One warning
        can cover several pages: when an automatic OCR run of selected pages fails as a whole,
        it adds one warning and one record for each selected page. A failure of the OCR fallback
        for the whole document adds a warning and no record. Repeated failures on one page make
        one record.
      required:
        - page
        - error
        - recovered
      properties:
        error:
          type: string
          description: OCR backend error text.
        page:
          type: integer
          format: int32
          description: One-based page number in the source document.
          minimum: 0
        recovered:
          type: boolean
          description: Whether content for this page was retained or recovered through another route.
    OcrPipelineConfig:
      type: object
      description: Multi-backend OCR pipeline with quality-based fallback.
      properties:
        quality_thresholds:
          $ref: '#/components/schemas/OcrQualityThresholds'
          description: Quality evaluation thresholds for fallback decisions
        stages:
          type: array
          items:
            $ref: '#/components/schemas/OcrPipelineStage'
          description: Ordered list of backends to try (sorted by priority descending)
      additionalProperties: false
    OcrPipelineStage:
      type: object
      description: Single backend stage in the OCR pipeline.
      required:
        - backend
      properties:
        backend:
          type: string
          description: |-
            Backend name for this pipeline stage. Must be one this deployment has
            compiled in and registered; the accepted set is deployment-dependent
            and is rejected with a 400 when not recognized.
        backend_options:
          type: object
          description: |-
            Per-call options for this stage's backend, as a JSON object.

            The top-level `ocr.backend_options` has no effect once a pipeline is
            set, so a stage carries its own.
        language:
          type:
            - string
            - "null"
          description: Language override for this stage
        priority:
          type:
            - integer
            - "null"
          format: int32
          description: |-
            Priority (higher = tried first). Omit to take the default of 100.

            Optional so that an omitted priority stays omitted. As a plain `u32` it
            deserialized to zero, serialized as `"priority": 0` into the job
            message, and the mapping layer could no longer tell a caller who asked
            for the lowest priority from one who never mentioned it.
          minimum: 0
        tesseract_config:
          $ref: '#/components/schemas/TesseractConfig'
          description: Tesseract config override for this stage
        vlm_config:
          type: object
          description: VLM config override for this stage
      additionalProperties: false
    OcrPoint:
      type: object
      required:
        - x
        - y
      properties:
        x:
          type: integer
        y:
          type: integer
    OcrQualityThresholds:
      type: object
      description: Quality thresholds for OCR fallback decisions.
      properties:
        alnum_ws_ratio_threshold:
          type:
            - number
            - "null"
          format: double
        critical_fragmented_word_ratio:
          type:
            - number
            - "null"
          format: double
        max_fragmented_word_ratio:
          type:
            - number
            - "null"
          format: double
        min_alnum_ratio:
          type:
            - number
            - "null"
          format: double
        min_avg_word_length:
          type:
            - number
            - "null"
          format: double
        min_consecutive_repeat_ratio:
          type:
            - number
            - "null"
          format: double
        min_garbage_chars:
          type:
            - integer
            - "null"
          format: int64
          minimum: 0
        min_meaningful_word_len:
          type:
            - integer
            - "null"
          format: int64
          minimum: 0
        min_meaningful_words:
          type:
            - integer
            - "null"
          format: int64
          minimum: 0
        min_non_whitespace_per_page:
          type:
            - number
            - "null"
          format: double
        min_total_non_whitespace:
          type:
            - integer
            - "null"
          format: int64
          minimum: 0
        min_words_for_avg_length_check:
          type:
            - integer
            - "null"
          format: int64
          minimum: 0
        min_words_for_repeat_check:
          type:
            - integer
            - "null"
          format: int64
          minimum: 0
        non_text_min_chars:
          type:
            - integer
            - "null"
          format: int64
          minimum: 0
        pipeline_min_quality:
          type:
            - number
            - "null"
          format: double
        substantive_min_chars:
          type:
            - integer
            - "null"
          format: int64
          minimum: 0
      additionalProperties: false
    OcrRotation:
      type: object
      description: Rotation information for an OCR element.
      required:
        - angle_degrees
      properties:
        angle_degrees:
          type: number
          format: double
          description: Rotation angle in degrees (0, 90, 180, 270 for PaddleOCR).
        confidence:
          type:
            - number
            - "null"
          format: double
          description: Confidence score for the rotation detection.
    OcrStrategy:
      oneOf:
        - type: object
          description: OCR only where the native text layer fails a quality check (default).
          required:
            - mode
          properties:
            mode:
              type: string
              enum:
                - auto
        - type: object
          description: Also OCR every page that looks like a scan at or above `min_confidence`.
          required:
            - min_confidence
            - mode
          properties:
            min_confidence:
              type: number
              format: double
            mode:
              type: string
              enum:
                - scanned_pages
      description: |-
        Which pages of a PDF go to OCR when neither `force_ocr` nor
        `force_ocr_pages` applies.

        Mirrors xberg's `OcrStrategy`, tag and variant names included, so a caller
        who reads the library documentation can write the same object here.

        `auto` OCRs a page only when its native text layer fails a quality check,
        and a scanner's invisible text layer passes that check. `scanned_pages`
        additionally OCRs every page that grades at or above `min_confidence` on
        scan detection, which is the knob between "OCR everything" and "OCR
        nothing": a lower threshold sends more pages to recognition, a higher one
        sends fewer.

        `min_confidence` is in `[0.0, 1.0]` and anything else is a 400. xberg's own
        default is 0.70, and it is what an omitted `ocr_strategy` keeps.

        Rejects an unrecognised key on either variant, unlike the wire type
        it mirrors (`messages::jobs::OcrStrategy`): this DTO sits on the live
        request path, which ADR-0082 keeps strict, so
        `{"mode":"auto","min_confidence":0.3}` is a 400 naming the field
        rather than a threshold parsed and silently dropped. The wire type
        stays lenient on purpose — it is reachable from the worker's tolerant
        decode paths, and ADR-0082's amendment measured that adding
        `deny_unknown_fields` to a tagged enum reachable from there can
        permanently strand a stored job instead of merely dropping a field.

        `Deserialize` is hand-written rather than derived because serde's
        `deny_unknown_fields` only rejects an extra field beside a fieldless
        variant when that variant declares its (empty) field set with braces
        (`Auto {}`), and a struct variant cannot carry `#[default]`. So the
        public enum keeps `Auto` a plain unit variant — every call site
        matches it that way — and deserializes through a private shadow enum
        that uses the brace form purely to get the rejection. xberg's own
        `OcrStrategy` uses the identical split for the identical reason.
    PageBoundary:
      type: object
      description: |-
        Byte offset boundary for a page.

        Tracks where a specific page's content starts and ends in the main content string,
        enabling mapping from byte positions to page numbers. Offsets are guaranteed to be
        at valid UTF-8 character boundaries when using standard String methods (push_str, push, etc.).
      required:
        - byte_start
        - byte_end
        - page_number
      properties:
        byte_end:
          type: integer
          description: Byte offset where this page ends in the content string (UTF-8 valid boundary, exclusive)
          minimum: 0
        byte_start:
          type: integer
          description: Byte offset where this page starts in the content string (UTF-8 valid boundary, inclusive)
          minimum: 0
        page_number:
          type: integer
          format: int32
          description: Page number (1-indexed)
          minimum: 0
    PageClassification:
      type: object
      description: Classification result for a single page.
      required:
        - page_number
        - labels
      properties:
        labels:
          type: array
          items:
            $ref: '#/components/schemas/ClassificationLabel'
          description: |-
            Labels assigned to the page. Single-label classification yields exactly one
            entry; multi-label classification yields any subset of the configured label set.
        page_number:
          type: integer
          format: int32
          description: 1-indexed page number this classification belongs to.
          minimum: 0
    PageConfig:
      type: object
      description: Page extraction configuration.
      properties:
        extract_pages:
          type:
            - boolean
            - "null"
          description: |-
            Extract pages as separate array in result. Omit to keep the library default.

            Optional so that an omitted key stays omitted on the wire. A plain `bool`
            serialized `false` into the job message, and the conversion wrote that
            over the library default as if the caller had asked for it.
        insert_page_markers:
          type:
            - boolean
            - "null"
          description: Insert page markers in content text. Omit to keep the library default.
        keep_native_content:
          type: boolean
          description: Retain a PDF page's native text when OCR replaces its content. Omit to keep the library default.
        marker_format:
          type:
            - string
            - "null"
          description: Page marker format template (e.g., "\n\n<!-- PAGE {page_num} -->\n\n")
      additionalProperties: false
    PageContent:
      type: object
      description: |-
        Content for a single page/slide.

        When page extraction is enabled, documents are split into per-page content
        with associated tables and images mapped to each page.

        # Performance

        Uses Arc-wrapped tables and images for memory efficiency:
        - `Vec<Arc<Table>>` enables zero-copy sharing of table data
        - `Vec<Arc<ExtractedImage>>` enables zero-copy sharing of image data
        - Maintains exact JSON compatibility via custom Serialize/Deserialize

        This reduces memory overhead for documents with shared tables/images
        by avoiding redundant copies during serialization.
      required:
        - page_number
        - content
      properties:
        content:
          type: string
          description: Text content for this page
        hierarchy:
          $ref: '#/components/schemas/PageHierarchy'
          description: |-
            Hierarchy information for the page (when hierarchy extraction is enabled)

            Contains text hierarchy levels (H1-H6) extracted from the page content.
        image_indices:
          type: array
          items:
            type: integer
            format: int32
            minimum: 0
          description: |-
            Indices into `ExtractedDocument.images` for images found on this page.

            Each value is a zero-based index into the top-level `images` collection.
            Only populated when `extract_images = true` in the extraction config.
        image_preprocessing:
          $ref: '#/components/schemas/ImagePreprocessingMetadata'
          description: OCR image preprocessing applied to this page's raster.
        is_blank:
          type:
            - boolean
            - "null"
          description: |-
            Whether this page is blank (no meaningful text content)

            Determined during extraction based on text content analysis.
            A page is blank if it has fewer than 3 non-whitespace characters
            and contains no tables or images.
        layout_regions:
          type:
            - array
            - "null"
          items:
            $ref: '#/components/schemas/LayoutRegion'
          description: |-
            Layout detection regions for this page (when layout detection is enabled).

            Contains detected layout regions with class, confidence, bounding box,
            and area fraction. Only populated when layout detection is configured.
        native_content:
          type:
            - string
            - "null"
          description: |-
            Text layer of this page, kept when OCR output replaced it in `content`.

            Set only when `PageConfig::keep_native_content` is on, an OCR route replaced this page's `content`, and the
            text layer is not blank and differs from the OCR text. `None` on every other page. PDF only.
        ocr_confidence:
          $ref: '#/components/schemas/PageOcrConfidence'
          description: Aggregate OCR confidence for this page. `None` when the page was not OCR'd.
        page_number:
          type: integer
          format: int32
          description: Page number (1-indexed)
          minimum: 0
        section_name:
          type:
            - string
            - "null"
          description: |-
            Section name this slide belongs to (PPTX only).

            PowerPoint sections group slides into logical chapters (`<p:sectionLst>` in
            `ppt/presentation.xml`). Only populated when the source is a PPTX file and
            the slide belongs to a named section.
        sheet_name:
          type:
            - string
            - "null"
          description: |-
            Sheet name for this page (XLSX/ODS only).

            Each spreadsheet sheet maps to one `PageContent` entry. This field carries the
            sheet's display name as it appears in the workbook. `None` for all non-spreadsheet
            formats and for sheets with an empty name.
        speaker_notes:
          type:
            - string
            - "null"
          description: |-
            Speaker notes for this slide (PPTX only).

            Contains the text from the slide's notes pane (`ppt/notesSlides/notesSlide{N}.xml`).
            Only populated when the source is a PPTX file and notes are present.
        tables:
          type: array
          items:
            $ref: '#/components/schemas/Table'
          description: |-
            Tables found on this page (uses Arc for memory efficiency)

            Serializes as `Vec<Table>` for JSON compatibility while maintaining
            Arc semantics in-memory for zero-copy sharing.
    PageDimensions:
      type: object
      required:
        - width
        - height
      properties:
        height:
          type: number
        width:
          type: number
    PageHierarchy:
      type: object
      description: |-
        Page hierarchy structure containing heading levels and block information.

        Used when PDF text hierarchy extraction is enabled. Contains hierarchical
        blocks with heading levels (H1-H6) for semantic document structure.
      required:
        - block_count
      properties:
        block_count:
          type: integer
          format: int32
          description: Number of hierarchy blocks on this page
          minimum: 0
        blocks:
          type: array
          items:
            $ref: '#/components/schemas/HierarchicalBlock'
          description: Hierarchical blocks with heading levels
    PageInfo:
      type: object
      required:
        - number
      properties:
        dimensions:
          $ref: '#/components/schemas/PageDimensions'
          description: Dimensions in points (PDF) or pixels (images).
        has_vector_graphics:
          type: boolean
          description: |-
            Whether this page contains non-trivial vector graphics (paths, shapes, curves)

            Indicates the presence of vector-drawn content such as charts, diagrams,
            or geometric shapes (e.g., from Adobe InDesign, LaTeX TikZ). These are
            invisible to `ExtractedDocument.images` since they are not embedded as raster
            XObjects. Set to `true` when path count exceeds a heuristic threshold,
            signaling that downstream consumers may want to rasterize the page to
            capture this content.

            Only populated for PDFs; `None` for other document types.
        hidden:
          type:
            - boolean
            - "null"
          description: Whether this page is hidden (e.g., in presentations)
        image_count:
          type:
            - integer
            - "null"
          format: int32
          description: Number of images on this page
          minimum: 0
        is_blank:
          type:
            - boolean
            - "null"
          description: |-
            Whether this page is blank (no meaningful text, no images, no tables)

            A page is considered blank if it has fewer than 3 non-whitespace characters
            and contains no tables or images. This is useful for filtering out empty pages
            in scanned documents or PDFs with blank separator pages.
        number:
          type: integer
          format: int32
          description: Page number (1-indexed)
          minimum: 0
        table_count:
          type:
            - integer
            - "null"
          format: int32
          description: Number of tables on this page
          minimum: 0
        title:
          type:
            - string
            - "null"
          description: Page title (usually for presentations)
    PageOcrConfidence:
      type: object
      description: |-
        Aggregate OCR legibility score for a page, reported by the backend that produced its text.

        This is distinct from `OcrConfidence`, which scores a single detected element (a word or
        line) using detection/recognition confidence from the OCR engine itself. `PageOcrConfidence`
        is a page-level summary computed after noise filtering, intended for triage of which pages
        are worth a closer look, not for comparing OCR engines against each other.
      required:
        - word_count
        - backend
      properties:
        backend:
          type: string
          description: Name of the OCR backend that produced the page text.
        score:
          type:
            - number
            - "null"
          format: double
          description: |-
            Aggregate legibility score in `0.0..=1.0`, or `None` when the backend that
            produced this page does not report a calibrated legibility scale.

            Backends differ in what their confidence numbers mean (see `OcrConfidence`'s
            per-backend normalization), and not every backend maps onto a 0.0-1.0 legibility
            scale at all. When a backend has no such calibrated scale, this is `None` rather
            than a misleading number, and scores must never be compared across backends.
        word_count:
          type: integer
          format: int32
          description: |-
            Number of words the score was averaged over, AFTER noise filtering.

            A small `word_count` means the average is based on little evidence, so a high
            `score` next to a small `word_count` is not representative of the whole page.
          minimum: 0
    PageSpan:
      type: object
      description: |-
        A single page covered by a chunk, with an optional bounding box on that page.

        See `ChunkMetadata.page_spans` (#1295) for population semantics.
      required:
        - page
      properties:
        bbox:
          $ref: '#/components/schemas/BoundingBox'
          description: Bounding box on this page, if known.
        page:
          type: integer
          format: int32
          description: Page number (1-indexed).
          minimum: 0
    PageStructure:
      type: object
      description: |-
        Unified page structure for documents.

        Supports different page types (PDF pages, PPTX slides, Excel sheets)
        with character offset boundaries for chunk-to-page mapping.
      required:
        - total_count
        - unit_type
      properties:
        boundaries:
          type:
            - array
            - "null"
          items:
            $ref: '#/components/schemas/PageBoundary'
          description: |-
            Character offset boundaries for each page

            Maps character ranges in the extracted content to page numbers.
            Used for chunk page range calculation.
        pages:
          type:
            - array
            - "null"
          items:
            $ref: '#/components/schemas/PageInfo'
          description: Detailed per-page metadata (optional, only when needed)
        total_count:
          type: integer
          format: int32
          description: Total number of pages/slides/sheets
          minimum: 0
        unit_type:
          $ref: '#/components/schemas/PageUnitType'
          description: Type of paginated unit
    PageUnitType:
      type: string
      description: |-
        Type of paginated unit in a document.

        Distinguishes between different types of "pages" (PDF pages, presentation slides, spreadsheet sheets).
      enum:
        - page
        - slide
        - sheet
    PdfAnnotation:
      type: object
      description: A PDF annotation extracted from a document page.
      required:
        - annotation_type
        - page_number
      properties:
        annotation_type:
          $ref: '#/components/schemas/PdfAnnotationType'
          description: The type of annotation.
        author:
          type:
            - string
            - "null"
          description: Author/creator of the annotation (PDF `/T` entry).
        bounding_box:
          $ref: '#/components/schemas/BoundingBox'
          description: Bounding box of the annotation on the page.
        color:
          type:
            - string
            - "null"
          description: |-
            Annotation colour (PDF `/C` entry), normalised to a CSS-compatible
            `#rrggbb` hex string. Gray and CMYK colour spaces are converted to RGB.
        content:
          type:
            - string
            - "null"
          description: Text content of the annotation (e.g., comment text, link URL).
        marked_text:
          type:
            - string
            - "null"
          description: |-
            The document text covered by `quad_points`, recovered from the
            page content underneath the marked-up region. Populated for
            Highlight, Underline, StrikeOut, and Squiggly annotations when the
            underlying text could be recovered.
        modified:
          type:
            - string
            - "null"
          description: |-
            Last modification date of the annotation (PDF `/M` entry), as a raw
            PDF date string (e.g. `"D:20240115120000Z"`).
        page_number:
          type: integer
          format: int32
          description: Page number where the annotation appears (1-indexed).
          minimum: 0
        quad_points:
          type:
            - array
            - "null"
          items:
            $ref: '#/components/schemas/BoundingBox'
          description: |-
            Per-line bounding boxes derived from the annotation's `/QuadPoints`
            entry. Present for text markup annotations (Highlight, Underline,
            StrikeOut, Squiggly), one box per marked line/run of text.
        subject:
          type:
            - string
            - "null"
          description: Subject of the annotation (PDF `/Subj` entry).
    PdfAnnotationType:
      type: string
      description: Type of PDF annotation.
      enum:
        - text
        - highlight
        - link
        - stamp
        - underline
        - strike_out
        - squiggly
        - ink
        - square
        - circle
        - polygon
        - poly_line
        - line
        - caret
        - file_attachment
        - sound
        - movie
        - other
    PdfConfig:
      type: object
      description: PDF-specific extraction options.
      properties:
        bottom_margin_fraction:
          type:
            - number
            - "null"
          format: float
          description: Bottom margin fraction to skip (0.0-1.0)
        extract_annotations:
          type:
            - boolean
            - "null"
          description: Extract PDF annotations
        extract_images:
          type:
            - boolean
            - "null"
          description: Extract images from PDF
        extract_metadata:
          type:
            - boolean
            - "null"
          description: Extract PDF metadata. Defaults to true when omitted.
        extract_tables:
          type:
            - boolean
            - "null"
          description: Extract tables from PDF pages. Defaults to true when omitted.
        hierarchy:
          $ref: '#/components/schemas/HierarchyConfig'
          description: Heading hierarchy detection
        passwords:
          type:
            - array
            - "null"
          items:
            type: string
          description: PDF passwords to try
        top_margin_fraction:
          type:
            - number
            - "null"
          format: float
          description: Top margin fraction to skip (0.0-1.0)
      additionalProperties: false
    PdfFormField:
      type: object
      description: |
        A form field extracted from a PDF's AcroForm or XFA structure.

        Populated by the PDF extractor when `PdfConfig::extract_form_fields` is
        enabled and the document is a fillable form. Supports both AcroForm (standard)
        and XFA (XML Forms Architecture) layers. When both are present, AcroForm fields
        take priority (canonical fallback per PDF spec), and XFA-only fields are appended.
        The collection is empty for non-form PDFs and for non-PDF formats.
      required:
        - name
        - full_name
        - field_type
      properties:
        bbox:
          $ref: '#/components/schemas/BoundingBox'
          description: Widget bounding box on its page, if known.
        default_value:
          type:
            - string
            - "null"
          description: Default field value, if any.
        field_type:
          $ref: '#/components/schemas/FormFieldType'
          description: Classified field type.
        flags:
          type: integer
          format: int32
          description: Raw field-flags bitmask (read-only, required, multiline, …).
          minimum: 0
        full_name:
          type: string
          description: Fully-qualified field name (dotted path from the form root).
        max_length:
          type:
            - integer
            - "null"
          format: int32
          description: Maximum input length for text fields, if specified.
          minimum: 0
        name:
          type: string
          description: Partial field name (the leaf name within the field hierarchy).
        page:
          type:
            - integer
            - "null"
          format: int32
          description: |-
            1-indexed page the field's widget appears on. Currently always `None` for
            AcroForm fields; page assignment is a deferred enhancement requiring spatial
            analysis of widget annotations per page.
          minimum: 0
        tooltip:
          type:
            - string
            - "null"
          description: Tooltip / alternate field description, if present.
        value:
          type:
            - string
            - "null"
          description: Current field value, if any.
    PdfMetadata:
      type: object
      description: |-
        PDF-specific metadata.

        Contains metadata fields specific to PDF documents that are not in the common
        `Metadata` structure. Common fields like title, authors, keywords, and dates
        are at the `Metadata` level.
      required:
        - format_type
      properties:
        fabricated_text_pages:
          type:
            - array
            - "null"
          items:
            type: integer
            format: int32
            minimum: 0
          description: |-
            Pages whose text was dominated by fabricated character mappings (1-indexed):
            `MappingProvenance::Fallback`, a font whose glyph-to-Unicode mapping resolved to
            a value the extractor chose rather than read from the file (issue #1254). This is
            a fact about how the text was derived, independent of `scanned_pages`'s raster-based
            scan detection, and independent of whether the resulting text happens to look
            structurally like prose (issue #1667: a broken mapping that lands on ordinary
            letters and punctuation passes every character-shape check but is still fabricated).

            `None` when `OcrQualityThresholds::enable_provenance_ocr_routing` is `false`; empty
            when no page qualifies.
        format_type:
          type: string
          enum:
            - pdf
        height:
          type:
            - integer
            - "null"
          format: int64
          description: First page height in points (1/72 inch)
        implausible_text_pages:
          type:
            - array
            - "null"
          items:
            type: integer
            format: int32
            minimum: 0
          description: |-
            Pages whose native text layer reads as no real detectable language (1-indexed): a
            `/ToUnicode` CMap (or other mapping tier) that resolves every glyph to *a* character,
            consistently the WRONG one (e.g. a ROT-shifted mapping), so the page is structurally
            indistinguishable from real prose to `fabricated_text_pages`'s provenance check and to
            every character-shape heuristic (issue #1696; issue #1667's `quality_score: 1.0` with
            no warning on such a page is the same underlying gap). This is a content-plausibility
            fact, independent of `fabricated_text_pages` and of `scanned_pages`.

            `None` when `OcrQualityThresholds::enable_plausibility_ocr_routing` is `false`; empty
            when no page qualifies.
        is_encrypted:
          type:
            - boolean
            - "null"
          description: Whether the PDF is encrypted/password-protected
        layout_gate_reasons:
          type:
            - array
            - "null"
          items:
            type: string
          description: |-
            Why the `auto` layout gate selected or skipped each page.

            Index `i` is page `i + 1`. Snake_case values such as `multi_column`,
            `table_grid`, or `plain_text` (the skip reason). `None` unless layout
            detection ran with `LayoutStrategy::Auto`.
        layout_gated_pages:
          type:
            - array
            - "null"
          items:
            type: integer
            format: int32
            minimum: 0
          description: |-
            Pages the `auto` layout strategy skipped (1-indexed).

            `None` unless layout detection ran with `LayoutStrategy::Auto`; empty
            when the gate selected every page.
        page_count:
          type:
            - integer
            - "null"
          format: int32
          description: Total number of pages in the PDF document
          minimum: 0
        pdf_version:
          type:
            - string
            - "null"
          description: PDF version (e.g., "1.7", "2.0")
        producer:
          type:
            - string
            - "null"
          description: PDF producer (application that created the PDF)
        scanned_confidence:
          type:
            - number
            - "null"
          format: float
          description: |-
            How strongly the document's most scan-like page resembles a scan, in `[0.0, 1.0]`.

            `None` when the document could not be inspected. A full-page raster with no
            visible text scores at least `0.85`; a born-digital slide with a full-bleed
            background image scores `0.50`.
        scanned_pages:
          type:
            - array
            - "null"
          items:
            type: integer
            format: int32
            minimum: 0
          description: |-
            Pages that look like scans (1-indexed), using the default confidence threshold.

            `None` when the document could not be inspected; empty when no page qualifies.
        width:
          type:
            - integer
            - "null"
          format: int64
          description: First page width in points (1/72 inch)
    PiiCategory:
      oneOf:
        - type: string
          description: Email address (e.g. `user@example.com`).
          enum:
            - email
        - type: string
          description: Phone number in any common format.
          enum:
            - phone
        - type: string
          description: US Social Security Number.
          enum:
            - ssn
        - type: string
          description: Payment card number (Visa, Mastercard, Amex, etc.).
          enum:
            - credit_card
        - type: string
          description: Postal / ZIP code.
          enum:
            - postal_code
        - type: string
          description: IPv4 or IPv6 address.
          enum:
            - ip_address
        - type: string
          description: International Bank Account Number.
          enum:
            - iban
        - type: string
          description: SWIFT / BIC bank identifier code.
          enum:
            - swift_bic
        - type: string
          description: Date of birth.
          enum:
            - date_of_birth
        - type: string
          description: Person name, surfaced by the optional NER backend.
          enum:
            - person
        - type: string
          description: Organization name, surfaced by the optional NER backend.
          enum:
            - organization
        - type: string
          description: Location, surfaced by the optional NER backend.
          enum:
            - location
        - type: object
          description: |-
            Caller-supplied custom category (e.g. internal employee IDs).

            Surfaced by the redaction engine when a hit comes from
            custom term rules
            or custom pattern rules.
            The string is the label passed alongside the term/pattern. Use those
            fields rather than constructing `Custom` directly via the
            `categories` filter — the pattern engine cannot detect arbitrary text
            from a category name alone.
          required:
            - custom
          properties:
            custom:
              type: string
              description: |-
                Caller-supplied custom category (e.g. internal employee IDs).

                Surfaced by the redaction engine when a hit comes from
                custom term rules
                or custom pattern rules.
                The string is the label passed alongside the term/pattern. Use those
                fields rather than constructing `Custom` directly via the
                `categories` filter — the pattern engine cannot detect arbitrary text
                from a category name alone.
      description: PII categories the pattern engine recognises.
    PixelDimensions:
      type: object
      required:
        - width
        - height
      properties:
        height:
          type: integer
        width:
          type: integer
    PostProcessorConfig:
      type: object
      description: Post-processing pipeline configuration.
      properties:
        disabled_processors:
          type:
            - array
            - "null"
          items:
            type: string
          description: Blacklist of processors to disable (None = none)
        enabled:
          type:
            - boolean
            - "null"
          description: Enable post-processors. Defaults to true when omitted.
        enabled_processors:
          type:
            - array
            - "null"
          items:
            type: string
          description: Whitelist of processors to enable (None = all)
      additionalProperties: false
    PptxMetadata:
      type: object
      description: |-
        PowerPoint presentation metadata.

        Extracted from PPTX files containing slide counts and presentation details.
      required:
        - slide_count
        - slide_names
        - format_type
      properties:
        format_type:
          type: string
          enum:
            - pptx
        image_count:
          type:
            - integer
            - "null"
          format: int32
          description: Number of embedded images
          minimum: 0
        slide_count:
          type: integer
          format: int32
          description: Total number of slides in the presentation
          minimum: 0
        slide_names:
          type: array
          items:
            type: string
          description: Names of slides (if available)
        table_count:
          type:
            - integer
            - "null"
          format: int32
          description: Number of tables
          minimum: 0
    PresetCallMode:
      type: string
      description: Preferred call mode for a preset.
      enum:
        - text_only
        - vision_only
        - text_plus_vision
    PresetCategory:
      type: string
      description: High-level category used to group presets in the registry UI.
      enum:
        - finance
        - identity
        - legal
        - logistics
        - medical
        - hr
        - other
    PresetDetail:
      type: object
      description: Full preset detail, returned by `GET /v1/presets/{id}`.
      required:
        - id
        - version
        - schema_name
        - description
        - category
        - schema
        - system_prompt
        - preferred_call_mode
        - emit_citations
        - fingerprint
      properties:
        category:
          $ref: '#/components/schemas/PresetCategory'
        context_template:
          type:
            - string
            - "null"
        description:
          type: string
        emit_citations:
          type: boolean
        fingerprint:
          type: string
        id:
          type: string
          description: Stable, URL-safe preset identifier (lowercase snake_case slug, not a UUID).
        preferred_call_mode:
          $ref: '#/components/schemas/PresetCallMode'
        sample:
          $ref: '#/components/schemas/PresetSampleRef'
        schema:
          type: object
          description: JSON Schema (Draft 2020-12) describing the structured output shape.
        schema_name:
          type: string
        system_prompt:
          type: string
        tags:
          type: array
          items:
            type: string
        version:
          type: string
    PresetSampleRef:
      type: object
      description: Public pointer to bundled sample data exposed via `GET /v1/presets/{id}/sample`.
      required:
        - input_url
        - output_url
      properties:
        input_url:
          type: string
          description: URL path (relative to the API root) to fetch the sample input bytes.
        output_url:
          type: string
          description: URL path (relative to the API root) to fetch the reference output JSON.
    PresetSummary:
      type: object
      description: Lightweight projection of a preset, returned by `GET /v1/presets`.
      required:
        - id
        - version
        - schema_name
        - description
        - category
        - preferred_call_mode
        - emit_citations
        - fingerprint
      properties:
        category:
          $ref: '#/components/schemas/PresetCategory'
        description:
          type: string
        emit_citations:
          type: boolean
        fingerprint:
          type: string
          description: Stable sha256 fingerprint used by clients to detect updates.
        id:
          type: string
          description: Stable, URL-safe preset identifier (lowercase snake_case slug, not a UUID).
        preferred_call_mode:
          $ref: '#/components/schemas/PresetCallMode'
        schema_name:
          type: string
        tags:
          type: array
          items:
            type: string
        version:
          type: string
    PresignDocumentInput:
      type: object
      description: Document metadata for presigned upload (no bytes)
      required:
        - filename
        - mime_type
      properties:
        config:
          $ref: '#/components/schemas/FileExtractionConfig'
          description: |-
            Per-file extraction config override.
            Merged with batch-level `config` at confirm time. None = use batch default.
        filename:
          type: string
          description: Original filename
        mime_type:
          type: string
          description: MIME type of the document
      additionalProperties: false
    PresignUploadRequest:
      type: object
      description: Request body for presigning upload URLs
      required:
        - documents
      properties:
        config:
          $ref: '#/components/schemas/ExtractionConfig'
          description: Batch-level extraction configuration (applied to all documents)
        context:
          type:
            - object
            - "null"
          description: |-
            Free-form context, resolved the same way as `/v1/extract`'s
            `options.context` (`api_types::ExtractionOptions::context`). It
            renders into a curated or saved preset's context template when one
            is set. With no preset it is still validated and written into
            `structured_extraction.context` on the resolved config, unlike Xberg
            Pro's presign handler, which validates a standalone context the same
            way but discards it when no preset is set.
          additionalProperties:
            type: string
          propertyNames:
            type: string
        documents:
          type: array
          items:
            $ref: '#/components/schemas/PresignDocumentInput'
          description: Document metadata (no file data)
        preset:
          type:
            - string
            - "null"
          description: |-
            Curated preset identifier, resolved the same way as `/v1/extract`'s
            `options.preset` (`api_types::ExtractionOptions::preset`). Mutually
            exclusive with `saved_preset_id` and with an inline schema in `config`.
        saved_preset_id:
          type:
            - string
            - "null"
          description: |-
            Saved preset ID (project-scoped, user-created), resolved the same way
            as `/v1/extract`'s `options.saved_preset_id`. Mutually exclusive with
            `preset`.
        webhook:
          $ref: '#/components/schemas/WebhookConfig'
          description: Webhook configuration for async result delivery
      additionalProperties: false
    PresignUploadResponse:
      type: object
      description: Response from presign endpoint
      required:
        - batch_id
        - uploads
      properties:
        batch_id:
          type: string
          format: uuid
          description: Batch ID to pass to the confirm endpoint.
        uploads:
          type: array
          items:
            $ref: '#/components/schemas/PresignedUploadInfo'
          description: Per-document upload URLs
    PresignedUploadInfo:
      type: object
      description: Info about a single presigned upload
      required:
        - job_id
        - upload_url
        - object_key
        - method
        - expires_in_secs
      properties:
        expires_in_secs:
          type: integer
          format: int64
          description: Seconds until the URL expires
          minimum: 0
        job_id:
          type: string
          format: uuid
          description: Job ID for this document
        method:
          type: string
          description: HTTP method to use (PUT)
        object_key:
          type: string
          description: Storage object key
        upload_url:
          type: string
          description: Presigned URL to upload the document to
    PrimaryScore:
      oneOf:
        - type: object
          description: |-
            Vector similarity score, where a higher value is the better match.

            The collection's distance metric decides the scale: cosine gives the
            cosine similarity in `[-1, 1]`, inner product gives the unbounded inner
            product, and L2 gives `1 / (1 + distance)` in `(0, 1]`. Scores are
            comparable within a collection, not across collections.

            Every score in one response rises with relevance, but the scales differ,
            so a vector score and a full-text score are not comparable with each
            other even inside one breakdown.
          required:
            - score
            - kind
          properties:
            kind:
              type: string
              enum:
                - vector
            score:
              type: number
              format: float
              description: Score value.
        - type: object
          description: Full-text rank score.
          required:
            - score
            - kind
          properties:
            kind:
              type: string
              enum:
                - full_text
            score:
              type: number
              format: float
              description: Score value.
        - type: object
          description: RRF-fused vector + full-text.
          required:
            - rrf
            - kind
          properties:
            full_text:
              type:
                - number
                - "null"
              format: float
              description: |-
                Full-text component. `null` when the chunk matched only the
                vector leg.
            kind:
              type: string
              enum:
                - hybrid
            rrf:
              type: number
              format: float
              description: Reciprocal-rank-fusion combined score.
            vector:
              type:
                - number
                - "null"
              format: float
              description: |-
                Vector component. `null` when the chunk matched only the
                full-text leg.
      description: JSON-friendly mirror of vectorstore's internal `PrimaryScore`.
    ProcessingWarning:
      type: object
      description: |-
        A non-fatal warning from a processing pipeline stage.

        Captures errors from optional features that don't prevent extraction
        but may indicate degraded or incomplete results. Inspect these independently
        from `ExtractedDocument::quality_score`, which assesses retained text only.
      required:
        - source
        - message
      properties:
        message:
          type: string
          description: Human-readable description of what went wrong.
        source:
          type: string
          description: |-
            The pipeline stage or feature that produced this warning
            (e.g., "embedding", "chunking", "language_detection", "output_format").
    PromoteProfileRequest:
      type: object
      description: |-
        Request to promote a completed auto-tune result to a named, reusable
        tuning profile.
      required:
        - name
      properties:
        allow_unscored:
          type: boolean
          description: |-
            Promote the run even when no trial in it scored any document.

            Such a run's winning configuration extracted nothing, so the profile it produces is
            unproven and promotion refuses it by default. Set this when you know why the run
            scored nothing and you want its configuration anyway.
        name:
          type: string
          description: Name to give the promoted tuning profile.
          minLength: 1
    PropertyChange:
      type: object
      description: |-
        A single run-level or style-level property change.

        Used for revisions that change formatting rather than text content. `from`
        and `to` store normalized property values when the source format exposes
        them; either side may be absent when the format only records one side of the
        change.
      required:
        - name
      properties:
        from:
          type:
            - string
            - "null"
          description: Value before the change, when available.
        name:
          type: string
          description: Property name, such as `"bold"`, `"italic"`, `"font_size"`, or `"font_color"`.
        to:
          type:
            - string
            - "null"
          description: Value after the change, when available.
    ProxyOverride:
      type: object
      description: |-
        Reserved proxy override retained for wire compatibility. Proxy execution is
        unavailable while private-network denial is pinned; setting this object
        returns HTTP 400.
      required:
        - url
      properties:
        password:
          type:
            - string
            - "null"
          description: Optional password for proxy auth.
        url:
          type: string
          description: Proxy URL retained for wire compatibility; any value is rejected.
          example: http://192.0.2.1:8080
        username:
          type:
            - string
            - "null"
          description: Optional username for proxy auth.
    PstMetadata:
      type: object
      description: Outlook PST archive metadata.
      required:
        - message_count
        - format_type
      properties:
        format_type:
          type: string
          enum:
            - pst
        message_count:
          type: integer
          description: Total number of email messages found in the PST archive.
          minimum: 0
    QrBoundingBox:
      type: object
      description: Pixel-space bounding box of a QR code inside its source image.
      required:
        - x
        - y
        - width
        - height
      properties:
        height:
          type: integer
          format: int32
          description: Height of the bounding box in pixels.
          minimum: 0
        width:
          type: integer
          format: int32
          description: Width of the bounding box in pixels.
          minimum: 0
        x:
          type: integer
          format: int32
          description: Horizontal pixel offset of the bounding box top-left corner.
          minimum: 0
        y:
          type: integer
          format: int32
          description: Vertical pixel offset of the bounding box top-left corner.
          minimum: 0
    QrCode:
      type: object
      description: One QR code decoded from an extracted image.
      required:
        - payload
      properties:
        bbox:
          $ref: '#/components/schemas/QrBoundingBox'
          description: |-
            Bounding box of the QR code inside the source image, in pixel coordinates
            (`x`, `y` of the top-left corner; `width`, `height` of the rectangle).
            `None` if the decoder did not report a bounding box.
        confidence:
          type:
            - number
            - "null"
          format: float
          description: |-
            Detector-reported confidence in `[0.0, 1.0]`. `None` when the decoder
            does not expose confidence (the default `rqrr` backend always reports
            `Some` because successful decode implies high confidence).
        payload:
          type: string
          description: Decoded payload (text, URL, vCard string, …).
    RagJobKind:
      type: string
      description: Operation type for RAG async jobs
      enum:
        - ingest
        - reindex
        - migrate
    RagJobPendingReason:
      type: string
      description: Why a pending job has not started
      enum:
        - embedding_model_download
    RagJobProgress:
      type: object
      description: Progress details for long-running operations (migrate only)
      required:
        - documents_total
        - documents_processed
        - current_phase
      properties:
        current_phase:
          type: string
          description: 'Current processing phase (for migrate: "dual_write"|"index_build"|"swap"|"cleanup")'
        documents_processed:
          type: integer
          format: int64
          description: Documents successfully processed
          minimum: 0
        documents_total:
          type: integer
          format: int64
          description: Total documents to process
          minimum: 0
    RagJobState:
      type: string
      description: Job lifecycle state
      enum:
        - pending
        - running
        - completed
        - failed
    RagJobStatusResponse:
      type: object
      description: |-
        Unified RAG async job status response covering ingest, reindex, and migrate operations.

        The response shape varies depending on the operation type (`kind`):
        - **ingest**: status field reflects document ingestion progress (pending, indexed, failed)
        - **reindex**: status field reflects re-chunking and re-embedding progress
        - **migrate**: status and progress fields reflect embedding migration state
      required:
        - job_id
        - kind
        - state
        - created_at
      properties:
        collection_id:
          type:
            - string
            - "null"
          format: uuid
          description: Associated collection ID (for migrate only; absent for ingest/reindex)
        created_at:
          type: string
          format: date-time
          description: Timestamp when job was submitted (RFC3339)
        document_id:
          type:
            - string
            - "null"
          format: uuid
          description: Associated document ID (for ingest/reindex only; absent for migrate)
        error:
          type:
            - string
            - "null"
          description: Error message if state = failed
        error_code:
          type:
            - string
            - "null"
          description: Machine-readable cause when the failed job has a known classification.
        job_id:
          type: string
          description: Job identifier (ULID) for this async operation
        kind:
          $ref: '#/components/schemas/RagJobKind'
          description: 'Operation type: "ingest", "reindex", or "migrate"'
        pending_reason:
          $ref: '#/components/schemas/RagJobPendingReason'
          description: |-
            Why the job has not started yet. Absent unless state = pending, and
            absent when the job is simply queued.
        progress:
          $ref: '#/components/schemas/RagJobProgress'
          description: Progress details (only for migrate operations; null for ingest/reindex)
        state:
          $ref: '#/components/schemas/RagJobState'
          description: Current job state
    RakeParams:
      type: object
      description: RAKE-specific parameters.
      required:
        - min_word_length
        - max_words_per_phrase
      properties:
        max_words_per_phrase:
          type: integer
          description: 'Maximum words in a keyword phrase (default: 3).'
          minimum: 0
        min_word_length:
          type: integer
          description: 'Minimum word length to consider (default: 1).'
          minimum: 0
      additionalProperties: false
    ReadinessChecks:
      type: object
      description: Individual dependency check results
      required:
        - database
        - maintenance_database
        - nats
      properties:
        database:
          type: string
          description: Database connectivity
          example: ok
        maintenance_database:
          type: string
          description: Maintenance database connectivity.
          example: ok
        nats:
          type: string
          description: NATS connectivity
          example: ok
    ReadinessResponse:
      type: object
      description: Readiness check response with dependency status
      required:
        - status
        - checks
      properties:
        checks:
          $ref: '#/components/schemas/ReadinessChecks'
          description: Dependency check results
        status:
          type: string
          description: Overall readiness status
          example: ready
    RedactionFinding:
      type: object
      description: 'One redaction event: which span was rewritten, why, and with what.'
      required:
        - start
        - end
        - category
        - strategy
        - replacement_token
      properties:
        category:
          $ref: '#/components/schemas/PiiCategory'
          description: PII category that fired this redaction.
        end:
          type: integer
          format: int32
          description: Byte-offset end (exclusive) in the original `ExtractedDocument::content`.
          minimum: 0
        replacement_token:
          type: string
          description: |-
            String that replaced the original mention. Always present; for `Drop` the
            replacement is the empty string.
        start:
          type: integer
          format: int32
          description: Byte-offset start in the original (pre-redaction) `ExtractedDocument::content`.
          minimum: 0
        strategy:
          $ref: '#/components/schemas/RedactionStrategy'
          description: Strategy applied to this finding (mask, hash, token-replace, drop).
    RedactionReport:
      type: object
      description: |-
        Audit report describing what the redaction processor found and how it replaced it.

        The redactor returns this alongside the rewritten content so compliance, replay, and
        audit-log consumers can see exactly what fired. Offsets are relative to the *original*
        pre-redaction `content` and are intended for audit reconstruction only — the original
        bytes are dropped at the end of the pipeline.
      required:
        - findings
        - total_redacted
      properties:
        findings:
          type: array
          items:
            $ref: '#/components/schemas/RedactionFinding'
          description: Individual redaction findings in original-source byte order.
        total_redacted:
          type: integer
          format: int32
          description: Total number of redactions applied across the document.
          minimum: 0
    RedactionStrategy:
      type: string
      description: Strategy applied when a PII match is rewritten.
      enum:
        - mask
        - hash
        - token_replace
        - drop
    ReindexRequest:
      type: object
      description: Request to reindex a document (re-chunk, re-enrich, re-embed)
      properties:
        re_extract:
          type: boolean
          description: If true, re-extract from source object via xberg before re-chunking
        webhook:
          $ref: '#/components/schemas/WebhookConfig'
          description: Optional webhook for reindex completion/failure delivery.
      additionalProperties: false
    RelationshipKind:
      type: string
      description: Semantic kind of a relationship between document elements.
      enum:
        - footnote_reference
        - citation_reference
        - internal_link
        - caption
        - label
        - toc_entry
        - cross_reference
    RerankFallbackReason:
      type: string
      description: Why a requested rerank did not order the results.
      enum:
        - timeout
        - error
    RerankOutcomeReport:
      oneOf:
        - type: object
          description: Neither the caller nor the collection asked for a rerank.
          required:
            - status
          properties:
            status:
              type: string
              enum:
                - not_requested
        - type: object
          description: A rerank ran and the results carry its order.
          required:
            - kind
            - status
          properties:
            kind:
              $ref: '#/components/schemas/RerankerKind'
              description: The reranker that produced the order.
            status:
              type: string
              enum:
                - applied
        - type: object
          description: A rerank was asked for and the results carry the primary order instead.
          required:
            - kind
            - reason
            - status
          properties:
            kind:
              $ref: '#/components/schemas/RerankerKind'
              description: The reranker that was asked for.
            reason:
              $ref: '#/components/schemas/RerankFallbackReason'
              description: Why its order is not the one returned.
            status:
              type: string
              enum:
                - fell_back
      description: |-
        What happened to the rerank stage of a retrieve.

        One field with three exclusive states, because the field this replaces
        reported the same absence for "no rerank was asked for" and for "a rerank
        was asked for and did not serve the request". A caller could tell those
        apart only by asking for `include.score_breakdown` and reading a per-chunk
        string, so a request that was not served returned success and looked
        ordinary.
    RerankerKind:
      type: string
      description: |-
        Wire-shape mirror of the internal `RerankerKind` — the response shape stays
        stable even when we add variants internally.
      enum:
        - preset
        - llm
    RerankerSpec:
      oneOf:
        - type: object
          description: Local ONNX cross-encoder preset.
          required:
            - name
            - type
          properties:
            name:
              type: string
              description: |-
                Model name, for example `bge-reranker-v2-m3`, or one of the
                aliases `fast`, `balanced`, `quality`, `multilingual`. An alias
                can point at a different model between releases; name the model
                to pin it. The RAG concepts guide lists the shipped models.
            type:
              type: string
              enum:
                - preset
        - type: object
          description: Platform-managed LLM reranker (provider + API key from Helm env).
          required:
            - type
          properties:
            model:
              type:
                - string
                - "null"
              description: |-
                Optional model override (e.g. `"gemini-1.5-pro"`). When
                `None`, the platform default is used.
            type:
              type: string
              enum:
                - llm
      description: |-
        Caller-specified reranker. The Llm variant carries an optional
        model override that picks from the platform-supported set.
    RetrieveMode:
      type: string
      description: Retrieve mode discriminant.
      enum:
        - vector
        - full_text
        - hybrid
    RetrieveRequest:
      type: object
      description: Retrieve request body.
      properties:
        candidate_multiplier:
          type:
            - integer
            - "null"
          format: int32
          description: |-
            1..=20, default 4. Ignored when rerank is inactive. The store
            pulls `top_k * candidate_multiplier` candidates from the
            primary stage so the reranker has more to choose from.
          maximum: 20
          minimum: 1
        filter:
          type: object
          description: Filter expression (deserialized by the vectorstore Filter IR).
        group_by_document:
          type: boolean
          description: Collapse to one chunk per document.
        include:
          $ref: '#/components/schemas/IncludeMask'
          description: Optional response field mask.
        mode:
          $ref: '#/components/schemas/RetrieveMode'
          description: Retrieve mode. Defaults to `hybrid`.
        query:
          type:
            - string
            - "null"
          description: |-
            Text query. Required for full_text and hybrid modes; optional
            for vector mode when `query_vector` is supplied.
          maxLength: 8192
          minLength: 1
          pattern: .*\S.*
        query_vector:
          type:
            - array
            - "null"
          items:
            type: number
            format: float
          description: |-
            BYO query vector. Length must equal the collection's
            `embedding_dim`. Forbidden when `mode = full_text`.
        rerank:
          oneOf:
            - $ref: '#/components/schemas/RerankerSpec'
              description: |-
                Three-state rerank: absent (collection default) | null
                (disable) | present (override). The OpenAPI schema documents
                this as `Option<RerankerSpec>` — callers control the third
                state by simply omitting the field.
            - type: "null"
        top_k:
          type: integer
          format: int32
          description: Number of top results to return. `1..=200`, default 10.
          maximum: 200
          minimum: 1
      additionalProperties: false
    RetrieveResponse:
      type: object
      description: Retrieve response body.
      required:
        - mode
        - rerank
        - latency_breakdown
        - chunks
      properties:
        chunks:
          type: array
          items:
            $ref: '#/components/schemas/RetrievedChunk'
          description: Result chunks (relevance-ordered).
        latency_breakdown:
          $ref: '#/components/schemas/LatencyBreakdown'
          description: Latency breakdown.
        mode:
          $ref: '#/components/schemas/RetrieveMode'
          description: Mode used.
        rerank:
          $ref: '#/components/schemas/RerankOutcomeReport'
          description: What happened to the rerank stage. Always present.
        vector_candidates:
          $ref: '#/components/schemas/VectorCandidateReport'
          description: HNSW vector-leg completeness before fusion or reranking; absent for other retrieval paths.
    RetrievedChunk:
      type: object
      description: Single retrieved chunk in the response.
      required:
        - id
        - document_id
        - ordinal
        - score
        - chunk_metadata
      properties:
        chunk_metadata:
          type: object
          description: Chunk-level metadata.
          additionalProperties: {}
          propertyNames:
            type: string
        content:
          type:
            - string
            - "null"
          description: Chunk content. Present iff `include.content`.
        document:
          $ref: '#/components/schemas/RetrievedDocumentSummary'
          description: Parent document summary. Present iff `include.document`.
        document_id:
          type: string
          format: uuid
          description: Parent document ID.
        external_id:
          type:
            - string
            - "null"
          description: Caller-supplied external_id, if any.
        id:
          type: string
          format: uuid
          description: Chunk ID.
        ordinal:
          type: integer
          format: int32
          description: Position within document.
          minimum: 0
        score:
          type: number
          format: float
          description: Effective score after rerank (or primary score when rerank off).
        score_breakdown:
          $ref: '#/components/schemas/ScoreBreakdown'
          description: Score detail. Present iff `include.score_breakdown`.
    RetrievedDocumentSummary:
      type: object
      description: |-
        Wire shape of `RetrievedChunk.document` — the parent document summary
        returned when `include.document` is set.

        Mirrors `vectorstore::DocumentSummary`'s wire shape for documentation
        purposes only, without adding a dependency on `crates/vectorstore` (see
        the module-level doc on the `api-types`/`vectorstore` boundary above).
        `RetrievedChunk.document` stays `serde_json::Value` at rest; this type
        is used solely via `#[schema(value_type = ...)]` to give it a shape.
      required:
        - id
        - keywords
        - labels
        - entities
        - metadata
        - ingested_at
      properties:
        entities:
          type: array
          items: {}
          description: |-
            Named entities. Opaque item shape here; the engine type is
            enrichment-pipeline output.
        external_id:
          type: string
          description: Caller-supplied external identifier, if any.
        id:
          type: string
          format: uuid
          description: Document ID.
        ingested_at:
          type: string
          format: date-time
          description: Timestamp when the document was ingested.
        keywords:
          type: array
          items:
            type: string
          description: Extracted keywords.
        labels:
          type: array
          items: {}
          description: |-
            Labels matched from the collection's label vocabulary. Opaque item
            shape here; the engine type is per-collection configuration.
        metadata:
          type: object
          description: Document-level metadata.
          additionalProperties: {}
          propertyNames:
            type: string
        mime:
          type: string
          description: MIME type.
        title:
          type: string
          description: Document title.
    RevisionAnchor:
      oneOf:
        - type: object
          description: Body paragraph, identified by its zero-based index in the document flow.
          required:
            - index
            - type
          properties:
            index:
              type: integer
              description: Zero-based index of the paragraph in document order.
              minimum: 0
            type:
              type: string
              enum:
                - paragraph
        - type: object
          description: Cell inside a table.
          required:
            - row
            - col
            - table_index
            - type
          properties:
            col:
              type: integer
              description: Zero-based column index within the table.
              minimum: 0
            row:
              type: integer
              description: Zero-based row index within the table.
              minimum: 0
            table_index:
              type: integer
              description: Zero-based index of the table in document order.
              minimum: 0
            type:
              type: string
              enum:
                - table_cell
        - type: object
          description: Page, identified by its zero-based index.
          required:
            - index
            - type
          properties:
            index:
              type: integer
              description: Zero-based page index.
              minimum: 0
            type:
              type: string
              enum:
                - page
        - type: object
          description: Presentation slide, identified by its zero-based index.
          required:
            - index
            - type
          properties:
            index:
              type: integer
              description: Zero-based slide index.
              minimum: 0
            type:
              type: string
              enum:
                - slide
        - type: object
          description: Spreadsheet cell or range, identified by sheet index and optional name.
          required:
            - index
            - type
          properties:
            index:
              type: integer
              description: Zero-based sheet index.
              minimum: 0
            name:
              type:
                - string
                - "null"
              description: Sheet display name when available.
            type:
              type: string
              enum:
                - sheet
      description: Best-effort document location for a revision.
    RevisionDelta:
      type: object
      description: |-
        The content changes that make up a single revision.

        For insertions and deletions the `content` field carries the added/removed
        lines as `DiffLine::Added` / `DiffLine::Removed` entries. For format
        changes, `property_changes` carries normalized before/after formatting
        values when the source document exposes them.
      required:
        - content
        - table_changes
      properties:
        content:
          type: array
          items:
            $ref: '#/components/schemas/DiffLine'
          description: Line-level content changes for this revision.
        property_changes:
          type: array
          items:
            $ref: '#/components/schemas/PropertyChange'
          description: Formatting or metadata property changes for this revision.
        table_changes:
          type: array
          items:
            $ref: '#/components/schemas/CellChange'
          description: Cell-level table changes for this revision.
    RevisionKind:
      type: string
      description: Semantic classification of a tracked change.
      enum:
        - insertion
        - deletion
        - format_change
        - comment
    RoutingFallbackReason:
      type: string
      description: Why a routing-enabled run produced a global-only profile.
      enum:
        - no_valid_route
        - insufficient_holdout
        - confidence_below_threshold
    RoutingOutcome:
      oneOf:
        - type: object
          description: Per-document routing was disabled for the run.
          required:
            - status
          properties:
            status:
              type: string
              enum:
                - not_requested
        - type: object
          description: Learned routes improved on the global configuration and were kept.
          required:
            - status
          properties:
            status:
              type: string
              enum:
                - routes_kept
        - type: object
          description: The run requested routing but retained only the global configuration.
          required:
            - reason
            - status
          properties:
            reason:
              $ref: '#/components/schemas/RoutingFallbackReason'
              description: Why the learned routes were not retained.
            status:
              type: string
              enum:
                - global_only
      description: Outcome of per-document routing for an auto-tune run.
    SavedPresetDetail:
      type: object
      description: Response when retrieving a saved preset.
      required:
        - id
        - name
        - schema
        - preferred_call_mode
        - emit_citations
        - created_at
        - updated_at
      properties:
        context_template:
          type:
            - string
            - "null"
          description: Optional context template.
        created_at:
          type: string
          format: date-time
          description: ISO 8601 creation timestamp.
        description:
          type:
            - string
            - "null"
          description: Optional description.
        emit_citations:
          type: boolean
          description: |-
            Whether to emit citations in results.

            Stored and returned verbatim, but **not applied at extraction time** —
            see `preferred_call_mode`. Results produced from a saved preset carry no
            citations regardless of this value.
        id:
          type: string
          format: uuid
          description: Unique preset ID.
        name:
          type: string
          description: Preset name.
        preferred_call_mode:
          type: string
          description: |-
            Preferred orchestration path (`text_only`, `vision_only`, or
            `text_plus_vision`).

            Validated on write, stored, and returned verbatim, but
            **not applied at extraction time**. Call-mode selection belongs to the
            vision pipeline, which a job reaches only through the curated preset
            registry (`options.preset`); a saved preset never routes there. On Xberg
            Pro a saved preset contributes its `schema`, `system_prompt` and
            `context_template`, and nothing else.
        schema:
          type: object
          description: JSON Schema for the extraction result.
        system_prompt:
          type:
            - string
            - "null"
          description: Optional custom system prompt.
        updated_at:
          type: string
          format: date-time
          description: ISO 8601 last update timestamp.
    SavedPresetSummary:
      type: object
      description: Lightweight projection of a saved preset (for listings).
      required:
        - id
        - name
        - preferred_call_mode
        - created_at
      properties:
        created_at:
          type: string
          format: date-time
          description: ISO 8601 creation timestamp.
        description:
          type:
            - string
            - "null"
          description: Optional description.
        id:
          type: string
          format: uuid
          description: Unique preset ID.
        name:
          type: string
          description: Preset name.
        preferred_call_mode:
          type: string
          description: |-
            Preferred orchestration path (`text_only`, `vision_only`, or
            `text_plus_vision`).

            Validated on write, stored, and returned verbatim, but
            **not applied at extraction time**. Call-mode selection belongs to the
            vision pipeline, which a job reaches only through the curated preset
            registry (`options.preset`); a saved preset never routes there. On Xberg
            Pro a saved preset contributes its `schema`, `system_prompt` and
            `context_template`, and nothing else.
    SchemaCompliance:
      type: string
      description: |-
        Schema-validation outcome surfaced as one of three buckets.

        Fold into the combined confidence score without leaking internal validation
        error types.
      enum:
        - all_valid
        - partial_valid
        - all_invalid
    ScoreBreakdown:
      type: object
      description: Score detail for a single returned chunk.
      required:
        - primary
        - rank_primary
        - rank_final
      properties:
        primary:
          $ref: '#/components/schemas/PrimaryScore'
          description: Primary-stage score discriminant + value(s).
        rank_final:
          type: integer
          format: int32
          description: |-
            Rank position after reranking (1-indexed). Equal to
            `rank_primary` when rerank was off or fell back.
          minimum: 0
        rank_primary:
          type: integer
          format: int32
          description: Rank position in the primary-stage result set (1-indexed).
          minimum: 0
        rerank:
          type:
            - number
            - "null"
          format: float
          description: Reranker score when rerank was applied. `None` otherwise.
        rerank_fallback:
          type:
            - string
            - "null"
          description: |-
            Reason rerank was skipped or fell back, if applicable.
            Values: `"timeout"`.
    SecurityLimits:
      type: object
      description: |-
        Configuration for security limits across extractors.

        All limits are intentionally conservative to prevent DoS attacks
        while still supporting legitimate documents.
      properties:
        max_archive_size:
          type: integer
          description: Maximum uncompressed size for archives (500 MB)
          default: 524288000
          minimum: 0
        max_compression_ratio:
          type: integer
          description: Maximum compression ratio before flagging as potential bomb (100:1)
          default: 100
          minimum: 0
        max_content_size:
          type: integer
          description: Maximum string growth and decoded image allocation per document (512 MiB). This value may only be lowered.
          default: 536870912
          minimum: 0
        max_entity_length:
          type: integer
          description: |-
            Maximum length of any single XML entity / attribute / token (1 MiB).
            This is a per-token cap, NOT a total cap — billion-laughs class
            attacks where a single entity expands to hundreds of MB are caught
            here, while normal long text content (a paragraph, a CDATA block) is
            caught by `max_content_size` instead.
          default: 1048576
          minimum: 0
        max_files_in_archive:
          type: integer
          description: Maximum number of files in archive (10,000)
          default: 10000
          minimum: 0
        max_iterations:
          type: integer
          description: |-
            Maximum iterations per operation.

            For external redaction findings, this may lower but cannot raise the
            fixed 10,000-finding ceiling.
          default: 10000000
          minimum: 0
        max_nesting_depth:
          type: integer
          description: Maximum nesting depth for structures (1024)
          default: 1024
          minimum: 0
        max_pages:
          type:
            - integer
            - "null"
          description: |-
            Maximum number of pages (or slides, or frames) in a single document.
            `None` means unlimited.

            Checked once the count is known and before any per-page work (OCR, layout
            detection, rendering) starts. Byte-size limits do not bound page count: a
            scanned page can compress to a few kilobytes, so a document well under
            `max_content_size` or `max_archive_size` can still hold thousands of pages
            of per-page work. Defaults to `None` (unlimited) because a real ceiling
            here is workload-specific and a low default would silently reject
            legitimate large documents; callers that want a ceiling set this
            explicitly.

            Enforced for: PDF (`extractors::pdf`, page count via `xberg_native_pdf`/`lopdf`),
            PPTX (`extraction::pptx`, slide count from the archive's slide parts),
            Keynote (`extractors::iwork::keynote`, slide count from `Index/Slide-*.iwa`
            entry names), ODP (`extractors::odp`, `draw:page` count in `content.xml`),
            and multi-frame TIFF images built with the `ocr` feature
            (`extractors::image`, frame count via the `tiff` crate). Not enforced for
            any other format, including DOCX, ODT, XLSX, legacy PPT/DOC, Pages/Numbers,
            and TIFF images when the `ocr` feature is disabled: those formats either
            have no fixed "page" the crate can count without doing the expensive work
            itself (DOCX/ODT page count is a layout outcome, not a stored value), or
            have no per-page pipeline to gate at all. Setting `max_pages` on a
            document of an unenforced format is silently a no-op, not a guarantee.
          default: null
          minimum: 0
        max_table_cells:
          type: integer
          description: Maximum aggregate table cells per document (1000000). This value may only be lowered.
          default: 1000000
          minimum: 0
        max_xml_depth:
          type: integer
          description: Maximum XML depth (1024 levels)
          default: 1024
          minimum: 0
      additionalProperties: false
    SparseEmbedding:
      type: object
      description: |
        A sparse learned embedding: vocabulary term indices and their weights.

        `indices` are ascending vocabulary token ids; `values[i]` is the weight for
        `indices[i]`. The two arrays always have equal length. Only strictly-positive
        terms are retained, so the representation is genuinely sparse.
      required:
        - indices
        - values
      properties:
        indices:
          type: array
          items:
            type: integer
            format: int32
            minimum: 0
          description: Vocabulary token ids with non-zero weight, ascending.
        values:
          type: array
          items:
            type: number
            format: float
          description: Weights parallel to `SparseEmbedding.indices`.
    StructuredData:
      type: object
      description: Structured data (Schema.org, microdata, RDFa) block.
      required:
        - data_type
        - raw_json
      properties:
        data_type:
          $ref: '#/components/schemas/StructuredDataType'
          description: Type of structured data
        raw_json:
          type: string
          description: Raw JSON string representation
        schema_type:
          type:
            - string
            - "null"
          description: Schema type if detectable (e.g., "Article", "Event", "Product")
    StructuredDataType:
      type: string
      description: Structured data type classification.
      enum:
        - json-ld
        - microdata
        - rdfa
    StructuredExtractionConfig:
      type: object
      description: |-
        LLM-driven structured extraction.

        When set, the worker passes the extracted document text and the supplied
        JSON schema to an LLM provider (routed through liter-llm) and returns the
        validated JSON on `ExtractionResult::structured_output`.
      required:
        - llm
      properties:
        llm:
          $ref: '#/components/schemas/LlmConfig'
          description: LLM provider + model configuration.
        prompt:
          type:
            - string
            - "null"
          description: Custom Jinja2 prompt template. When omitted, the xberg default is used.
        schema:
          type: object
          description: |-
            JSON Schema (draft-07) the structured output must conform to. Optional
            only when `options.preset` or `options.saved_preset_id` supplies it.
        schema_description:
          type:
            - string
            - "null"
          description: Optional human-readable schema description supplied to the LLM.
        schema_name:
          type: string
          description: Schema name passed to the LLM's structured-output mode. Defaults to "extraction".
        strict:
          type: boolean
          description: Strict mode — output must match the schema exactly (provider-dependent).
      additionalProperties: false
    SummaryStrategy:
      type: string
      description: Summarisation strategy.
      enum:
        - extractive
        - abstractive
    Table:
      type: object
      description: |-
        Extracted table structure.

        Represents a table detected and extracted from a document (PDF, image, etc.).
        Tables are converted to both structured cell data and Markdown format.
      required:
        - cells
        - markdown
        - page_number
      properties:
        bounding_box:
          $ref: '#/components/schemas/BoundingBox'
          description: |-
            Bounding box of the table's position. Only populated when position data is
            available from the producing extractor.

            The coordinate space depends on how the table was produced, and callers must
            know which route produced a given `Table` before interpreting this field:

            - Tables extracted from a PDF's native content, and tables recognized on a
              scanned PDF page that went through xberg's OCR pipeline (`--force-ocr` /
              `--ocr-scanned-pages`), are in **PDF points with a bottom-left origin**
              (x0=left, y0=bottom, x1=right, y1=top; y increases upward). For the OCR
              case, the pipeline rescales the backend's raw pixel output into this space
              before it reaches `Table::bounding_box` — see
              `rescale_ocr_bboxes_to_page_points` in `extractors::pdf::ocr`.
            - Tables detected by OCR on a standalone image with no backing PDF page (for
              example extracting a bare PNG/JPEG/TIFF) are in **raster pixel coordinates
              with a top-left origin** (x0=left, y0=top, x1=right, y1=bottom; y increases
              downward) — the same convention the OCR backend (Tesseract, PaddleOCR,
              candle-based backends) or the layout detector reported them in. There is no
              PDF page geometry to rescale into for this case, so the raw pixel box is
              passed through unchanged.
        cell_styles:
          type: array
          items:
            $ref: '#/components/schemas/TableCellStyle'
          description: |-
            Paragraph styles carried by individual cells, for the cells that have one.

            Sparse and flat on purpose. A DOCX banner row -- row 0, one cell spanning the grid,
            styled `Heading1`..`Heading6` -- is what Word's navigation pane and a `TOC` field treat
            as the document outline, but as a table cell it reached consumers as anonymous text
            (GH#1587). `cells` keeps the bare text: prefixing it with `#` would put a markdown
            heading inside a table cell, which is invalid where it lands and would change text every
            existing consumer already reads. This list is the signal instead.

            Entries are only emitted for cells that actually carry a style, so an ordinary table
            serialises exactly as it did before. Indices are into `cells`.
        cells:
          type: array
          items:
            type: array
            items:
              type: string
          description: Table cells as a 2D vector (rows × columns)
        columns:
          type:
            - array
            - "null"
          items:
            type: string
          description: |-
            Header cells for this fragment, i.e. the first row of `cells`.

            Populated even when this fragment's own header row was merged away or
            physically lives in a sibling fragment (see `table_id`), so a single
            fragment is interpretable in isolation. `None` when no header row
            could be determined.
        markdown:
          type: string
          description: Markdown representation of the table
        page_number:
          type: integer
          format: int32
          description: Page number where the table was found (1-indexed)
          minimum: 0
        table_id:
          type:
            - string
            - "null"
          description: |-
            Stable identifier shared by every `tables[]` entry that represents a
            fragment of the same physical table.

            Assigned deterministically by the extraction pipeline (e.g. a
            sequential `"table-N"` in document order); never derived from
            randomness or wall-clock time, so the same input document always
            produces the same ids. Consumers can use it to reconcile the markdown
            blocks in `content` / `pages[].content` / `chunks[].content` with the
            structured entries in `tables[]`. `None` when the extractor did not
            assign one.

            Today, same-page fragments of one physical table are already merged
            into a single `tables[]` entry before ids are assigned (see PDF table
            stitching), so in practice `table_id` is unique per entry rather than
            shared across several. A table split across a page boundary is
            intentionally *not* linked — its per-page pieces get separate ids.
            Sharing one id across page-boundary fragments is a known possible
            future extension, not implemented yet.
    TableCellStyle:
      type: object
      description: |-
        The paragraph style a single table cell's text carries, located by grid position.

        Flat rather than a nested `Vec<Vec<Option<..>>>`: the nested shape marshals badly across the
        FFI bindings, and the data is sparse anyway. See `Table.cell_styles`.
      required:
        - row
        - col
      properties:
        col:
          type: integer
          format: int32
          description: Zero-indexed column of the cell this style belongs to.
          minimum: 0
        heading_level:
          type:
            - integer
            - "null"
          format: int32
          description: Outline level 1-6 when the style resolves to a heading, otherwise `None`.
          minimum: 0
        row:
          type: integer
          format: int32
          description: Zero-indexed row of the cell this style belongs to.
          minimum: 0
        style_name:
          type:
            - string
            - "null"
          description: Human-readable style name, e.g. `heading 2`.
    TableGrid:
      type: object
      description: |-
        Structured table grid with cell-level metadata.

        Stores row/column dimensions and a flat list of cells with position info.
      required:
        - rows
        - cols
        - cells
      properties:
        cells:
          type: array
          items:
            $ref: '#/components/schemas/GridCell'
          description: All cells in row-major order.
        cols:
          type: integer
          format: int32
          description: Number of columns in the table.
          minimum: 0
        rows:
          type: integer
          format: int32
          description: Number of rows in the table.
          minimum: 0
    TesseractConfig:
      type: object
      description: |-
        Tesseract OCR configuration.

        Provides fine-grained control over Tesseract OCR engine parameters.
        Most users can use the defaults, but these settings allow optimization
        for specific document types (invoices, handwriting, etc.).

        **This is the public-facing counterpart of `ocr::types::TesseractConfig`
        (the internal, engine-facing representation with `u8`/`String` fields instead of
        `i32`/`Vec<String>`).** They are two independent struct definitions bridged only by
        an explicit `From<&TesseractConfig> for crate::ocr::types::TesseractConfig` impl in
        `ocr/types.rs` — that conversion carries field *values* across, but each struct keeps
        its own `Default` impl, and the conversion does nothing to keep those two defaults in
        sync. Several production call sites (`extractors::image::apply_default_tesseract_psm`,
        `configured_region_ocr`, `sparse_image_ocr_fallback_config`) construct *this* struct's
        default and convert it, bypassing the internal struct's `Default` entirely — so if the
        two defaults disagree, standalone image OCR silently uses this struct's value while
        PDF-embedded OCR (which can reach the internal `Default` directly when no
        `tesseract_config` is set) uses the other. When changing a default here, also update
        `ocr::types::TesseractConfig::default`, and vice versa.
      properties:
        classify_use_pre_adapted_templates:
          type: boolean
          description: Use pre-adapted templates for character classification
          default: true
        enable_table_detection:
          type: boolean
          description: Enable automatic table detection and reconstruction
          default: true
        language:
          type: array
          items:
            type: string
          description: |-
            Language code(s) for OCR recognition. For Tesseract, languages are joined with "+".

            A list is the canonical form and the only form accepted by the binding
            object APIs (Python, Node, PHP, WASM, etc.): `["eng", "deu"]`. When
            deserializing from a config file, JSON body, or the REST/MCP API, a
            single string is also accepted, either as one code ("eng") or
            "+"-joined ("eng+deu").
          default:
            - eng
        language_model_ngram_on:
          type: boolean
          description: |-
            Enable N-gram language model.

            Kept on by default (see default settings and
            `ocr::types::TesseractConfig::language_model_ngram_on` for the rationale);
            keep this field's default in sync with the internal struct's.
          default: true
        min_confidence:
          type: number
          format: double
          description: |-
            Minimum confidence threshold (0.0-100.0).

            Words with confidence below this threshold may be rejected or flagged.
          default: 0
        oem:
          type: integer
          format: int32
          description: |-
            OCR Engine Mode (0-3).

            - 0: Legacy engine only
            - 1: Neural nets (LSTM) only (usually best)
            - 2: Legacy + LSTM
            - 3: Default (based on what's available)
          default: 3
        output_format:
          type: string
          description: Output format ("text" or "markdown")
          default: markdown
        preprocessing:
          $ref: '#/components/schemas/ImagePreprocessingConfig'
          description: |-
            Image preprocessing configuration.

            Controls how images are preprocessed before OCR. Can significantly
            improve quality for scanned documents or low-quality images.
        psm:
          type:
            - integer
            - "null"
          format: int32
          description: |-
            Page Segmentation Mode (1-13).

            PSM 0 is rejected: Tesseract's `PSM_OSD_ONLY` performs orientation and script
            detection with no character recognition, so it cannot satisfy a text-extraction
            request and previously yielded an empty document (GH#1586).

            `None` (the default) means the caller made no explicit choice: the extraction
            pipeline applies its own context-appropriate PSM (whole-image PSM 11, vertical-
            language PSM 5, layout-region PSM 6, or the sparse-text retry's PSM 3) exactly as
            it would with no `TesseractConfig` at all — see issue #1573. Setting any other
            field on this struct no longer changes that behaviour.

            A rendered PDF page (`force_ocr` / `force_ocr_pages` / scanned-page OCR) that is one
            full-page scan gets the same whole-image PSM as standalone image OCR of that raster
            (11, or 5 for a vertical language), so the scan needs no explicit `psm` (GH#1786). A
            page that is not a scan, such as forced OCR of a vector page, keeps the engine's
            automatic-layout PSM (3).

            Common explicit values:
            - 3: Fully automatic page segmentation (native engine default)
            - 6: Assume a single uniform block of text (WASM engine default — avoids
              layout-analysis hang)
            - 11: Sparse text with no particular order
          default: null
        table_column_threshold:
          type: integer
          format: int32
          description: Column threshold for table detection (pixels)
          default: 50
        table_min_confidence:
          type: number
          format: double
          description: Minimum confidence threshold for table detection (0.0-1.0)
          default: 0
        table_row_threshold_ratio:
          type: number
          format: double
          description: Row threshold ratio for table detection (0.0-1.0)
          default: 0.5
        tessedit_char_blacklist:
          type: string
          description: Blacklist of forbidden characters (empty = none forbidden)
          default: ""
        tessedit_char_whitelist:
          type: string
          description: Whitelist of allowed characters (empty = all allowed)
          default: ""
        tessedit_dont_blkrej_good_wds:
          type: boolean
          description: Don't reject good words during block-level processing
          default: true
        tessedit_dont_rowrej_good_wds:
          type: boolean
          description: Don't reject good words during row-level processing
          default: true
        tessedit_enable_dict_correction:
          type: boolean
          description: Enable dictionary correction
          default: true
        tessedit_use_primary_params_model:
          type: boolean
          description: Use primary language params model
          default: true
        textord_space_size_is_variable:
          type: boolean
          description: Variable-width space detection
          default: true
        thresholding_method:
          type: integer
          format: int32
          description: |-
            Tesseract image-binarization method (0-2): 0 = Otsu (default), 1 = LeptonicaOtsu,
            2 = Sauvola. Sent to Tesseract's integer `thresholding_method` engine variable.

            GH#1784: used to be a `bool` sent as `"true"`/`"false"`, silently ignored by
            Tesseract's integer parser. A config for the old field still deserializes: `false` ->
            `0` (Otsu, the prior no-op), `true` -> `1` (LeptonicaOtsu, the old "adaptive" doc).
          default: 0
        use_cache:
          type: boolean
          description: Enable OCR result caching
          default: true
      additionalProperties: false
    TextAnnotation:
      type: object
      description: |-
        Inline text annotation — byte-range based formatting and links.

        Annotations reference byte offsets into the node's text content,
        enabling precise identification of formatted regions.
      required:
        - start
        - end
        - kind
      properties:
        end:
          type: integer
          format: int32
          description: End byte offset in the node's text content (exclusive).
          minimum: 0
        kind:
          $ref: '#/components/schemas/AnnotationKind'
          description: Annotation type.
        start:
          type: integer
          format: int32
          description: Start byte offset in the node's text content (inclusive).
          minimum: 0
    TextDirection:
      type: string
      description: Text direction enumeration for HTML documents.
      enum:
        - ltr
        - rtl
        - auto
    TextMetadata:
      type: object
      description: |-
        Text/Markdown metadata.

        Extracted from plain text and Markdown files. Includes word counts and,
        for Markdown, structural elements like headers and links.
      required:
        - line_count
        - word_count
        - character_count
        - format_type
      properties:
        character_count:
          type: integer
          format: int32
          description: Number of characters
          minimum: 0
        code_blocks:
          type:
            - array
            - "null"
          items:
            $ref: '#/components/schemas/MarkdownCodeBlock'
          description: Code blocks (for Markdown files).
        format_type:
          type: string
          enum:
            - text
        headers:
          type:
            - array
            - "null"
          items:
            type: string
          description: Markdown headers (headings text only, for Markdown files)
        line_count:
          type: integer
          format: int32
          description: Number of lines in the document
          minimum: 0
        links:
          type:
            - array
            - "null"
          items:
            $ref: '#/components/schemas/MarkdownLink'
          description: Markdown links (for Markdown files).
        word_count:
          type: integer
          format: int32
          description: Number of words
          minimum: 0
    TokenReductionConfig:
      type: object
      description: Token reduction configuration for LLM consumption.
      properties:
        mode:
          type:
            - string
            - "null"
          description: |-
            Reduction mode: "off", "light", "moderate", "aggressive", "maximum".
            Defaults to "off" when omitted.
        preserve_important_words:
          type:
            - boolean
            - "null"
          description: Preserve capitalized/technical terms. Defaults to true when omitted.
      additionalProperties: false
    Translation:
      type: object
      description: |-
        Translation of the extracted content.

        Holds the translated rendition of `ExtractedDocument::content` and (when
        `preserve_markup` was requested) the translated `formatted_content`. Chunks
        are translated in place inside `ExtractedDocument::chunks[*].content` rather
        than duplicated here.
      required:
        - target_lang
        - content
      properties:
        content:
          type: string
          description: Translated plain-text body. Matches the shape of `ExtractedDocument::content`.
        formatted_content:
          type:
            - string
            - "null"
          description: |-
            Translated markup body (Markdown / HTML / etc.) when `preserve_markup` was
            enabled on the config. `None` otherwise.
        source_lang:
          type:
            - string
            - "null"
          description: BCP-47 source language. `None` when the translation backend was asked to detect.
        target_lang:
          type: string
          description: BCP-47 language tag the translation was produced into (e.g. `"de"`, `"fr-CA"`).
    TunableKnobInfo:
      type: object
      description: One knob that auto-tune can search over, for capability discovery.
      required:
        - knob
        - description
      properties:
        candidates:
          type: array
          items: {}
          description: Candidate values offered by this deployment, preserving their JSON types and search order.
        description:
          type: string
          description: Human-readable description of what this knob controls.
        knob:
          type: string
          description: Wire name of the knob, matching `autotune_core::search::Knob`'s serialized name.
        unavailable_reason:
          type:
            - string
            - "null"
          description: Why this knob has no usable candidate values, when it is unavailable.
    TuningProfileDetail:
      type: object
      description: Full detail of a promoted tuning profile.
      required:
        - id
        - name
        - profile
        - scores
        - created_at
      properties:
        created_at:
          type: string
          format: date-time
          description: ISO 8601 creation timestamp.
        id:
          type: string
          description: Unique tuning profile ID (ULID).
        name:
          type: string
          description: Tuning profile name.
        profile:
          type: object
          description: |-
            The tuning profile artifact. The concrete `TuningProfile` shape is
            defined in `autotune-core`; here it is opaque JSON.
        scores:
          type: object
          description: |-
            Scores recorded for this profile at promotion time. The concrete
            shape is defined in `autotune-core`; here it is opaque JSON.
    TuningProfileSummary:
      type: object
      description: Lightweight projection of a tuning profile (for listings).
      required:
        - id
        - name
        - created_at
      properties:
        created_at:
          type: string
          format: date-time
          description: ISO 8601 creation timestamp.
        id:
          type: string
          description: Unique tuning profile ID (ULID).
        name:
          type: string
          description: Tuning profile name.
    UpdateSavedPresetRequest:
      type: object
      description: Request to update a saved preset.
      properties:
        context_template:
          type:
            - string
            - "null"
          description: Optional context template.
        description:
          type:
            - string
            - "null"
          description: Optional description of the preset.
        emit_citations:
          type: boolean
          description: |-
            Whether to emit citations in results.

            Stored and returned verbatim, but **not applied at extraction time** —
            see `preferred_call_mode`. Results produced from a saved preset carry no
            citations regardless of this value.
        name:
          type: string
          description: |-
            Preset name. Trimmed before storage; a name that is entirely
            whitespace is rejected once trimmed.
          maxLength: 255
          minLength: 1
          pattern: \S
        preferred_call_mode:
          type: string
          description: |-
            Preferred orchestration path (`text_only`, `vision_only`, or
            `text_plus_vision`).

            Validated on write, stored, and returned verbatim, but
            **not applied at extraction time**. Call-mode selection belongs to the
            vision pipeline, which a job reaches only through the curated preset
            registry (`options.preset`); a saved preset never routes there. On Xberg
            Pro a saved preset contributes its `schema`, `system_prompt` and
            `context_template`, and nothing else.
          pattern: ^(text_only|vision_only|text_plus_vision)$
        schema:
          type: object
          description: JSON Schema (Draft 2020-12) for the extraction result.
        system_prompt:
          type:
            - string
            - "null"
          description: Optional custom system prompt override.
      additionalProperties: false
      minProperties: 1
    UpdateSavedPresetResponse:
      type: object
      description: Response from updating a saved preset.
      required:
        - success
        - message
      properties:
        message:
          type: string
          description: Human-readable confirmation message.
        success:
          type: boolean
          description: Always `true` on success (the endpoint returns an error status otherwise).
    UriKind:
      type: string
      description: Semantic classification of an extracted URI.
      enum:
        - hyperlink
        - image
        - anchor
        - citation
        - reference
        - email
    UrlInput:
      type: object
      description: A seed URL for crawl-based extraction.
      required:
        - url
      properties:
        crawl_config:
          type: object
          description: |-
            Per-URL crawl configuration override. Its non-null keys are merged
            over the request-level `crawl_config`, one level deep, and the result
            is the configuration this URL is crawled under. Kept as opaque JSON:
            the concrete crawl configuration schema is deployment-specific.
        url:
          type: string
          description: URL to crawl and extract documents from.
      additionalProperties: false
    UsageByMimeType:
      type: object
      description: Usage statistics broken down by MIME type.
      required:
        - documents
        - pages
        - failed
      properties:
        documents:
          type: integer
          format: int32
          description: Number of documents of this MIME type processed
        failed:
          type: integer
          format: int32
          description: Number of failed extractions for this MIME type
        pages:
          type: integer
          format: int64
          description: Total pages extracted from documents of this MIME type
    UsageResponse:
      type: object
      description: Current period usage and quota information.
      required:
        - period_start
        - period_end
        - total_pages
        - total_documents
        - total_failed
        - by_mime_type
      properties:
        by_mime_type:
          type: object
          description: Usage breakdown by MIME type
          additionalProperties:
            $ref: '#/components/schemas/UsageByMimeType'
          propertyNames:
            type: string
        period_end:
          type: string
          description: |-
            Inclusive final UTC date of a nonempty requested reporting window (ISO 8601).
            Zero-width requested windows retain the start date, so equal response dates
            can describe either one included UTC date or an empty requested window.
        period_start:
          type: string
          description: Start of the reporting period (ISO 8601 date)
        quota_limit:
          type:
            - integer
            - "null"
          format: int64
          description: Monthly page quota limit (null = unlimited)
        quota_remaining:
          type:
            - integer
            - "null"
          format: int64
          description: Remaining pages in monthly quota (null = unlimited)
        total_documents:
          type: integer
          format: int32
          description: Total documents processed in the period
        total_failed:
          type: integer
          format: int32
          description: Total failed extractions in the period
        total_pages:
          type: integer
          format: int64
          description: Total pages extracted in the period
    VectorCandidateReport:
      type: object
      description: Candidate availability evidence, not approximate nearest-neighbour accuracy or scan-limit cause.
      required:
        - budget
        - returned
        - unit
        - status
      properties:
        budget:
          type: integer
          format: int32
          description: Candidate budget, including any pre-rerank inflation.
          minimum: 0
        returned:
          type: integer
          format: int32
          description: Units the vector leg returned, before fusion or reranking.
          minimum: 0
        status:
          $ref: '#/components/schemas/VectorCandidateStatus'
          description: Evidence established in the retrieval snapshot.
        unit:
          $ref: '#/components/schemas/VectorCandidateUnit'
          description: Unit counted by the candidate budget.
    VectorCandidateStatus:
      type: string
      description: Candidate-set completeness; does not identify why an approximate scan stopped.
      enum:
        - budget_filled
        - eligible_set_exhausted
        - eligible_matches_omitted
        - unknown
    VectorCandidateUnit:
      type: string
      description: Candidate budget unit.
      enum:
        - chunks
        - documents
    VlmFallbackPolicy:
      oneOf:
        - type: object
          description: No VLM fallback (default). Uses single-backend mode only.
          required:
            - mode
          properties:
            mode:
              type: string
              enum:
                - disabled
        - type: object
          description: Try classical OCR first, then fall back to VLM below this quality threshold.
          required:
            - quality_threshold
            - mode
          properties:
            mode:
              type: string
              enum:
                - on_low_quality
            quality_threshold:
              type: number
              format: double
        - type: object
          description: Use VLM instead of classical OCR on pages selected for OCR.
          required:
            - mode
          properties:
            mode:
              type: string
              enum:
                - always
      description: |-
        VLM fallback policy for OCR operations.

        Determines whether and when to use the deployment's VLM model for OCR.
        Enterprise accepts active policies only when the operator configures a model;
        an explicit OCR pipeline takes precedence and ignores this policy.
    WebhookConfig:
      type: object
      description: Webhook configuration for async delivery of job results.
      required:
        - url
      properties:
        metadata:
          type:
            - object
            - "null"
          description: |-
            Optional key-value metadata included in the webhook payload. At most
            100 entries, keys 1-255 characters, values up to 1024 characters, and
            the whole map at most 8192 bytes JSON-encoded. Both tiers refuse a
            larger map with a 400.
          additionalProperties:
            type: string
          propertyNames:
            type: string
        secret:
          type:
            - string
            - "null"
          description: Optional HMAC-SHA256 secret for signing the webhook payload.
        url:
          type: string
          format: uri
          description: URL to deliver results to.
          minLength: 1
      additionalProperties: false
    WebhookDelivery:
      type: object
      description: Delivery diagnostics for a job's inline webhook.
      required:
        - status
        - attempt_count
      properties:
        attempt_count:
          type: integer
          format: int32
          description: Number of delivery attempts made so far.
          minimum: 0
        last_error:
          type:
            - string
            - "null"
          description: Bounded, sanitized diagnostic from the most recent failed attempt.
          maxLength: 2048
        status:
          $ref: '#/components/schemas/WebhookDeliveryStatus'
          description: Current delivery state.
    WebhookDeliveryDetailResponse:
      type: object
      description: |-
        Diagnostic payload previews for one authorized delivery; eligible for cleanup after seven days.
        Previews are capped at 4096 UTF-8 bytes and can contain tenant document content.
      required:
        - delivery
        - request_preview
        - request_truncated
        - response_truncated
      properties:
        delivery:
          $ref: '#/components/schemas/WebhookDeliveryResponse'
        request_preview:
          type: string
        request_truncated:
          type: boolean
        response_error:
          type:
            - string
            - "null"
          description: A failure to capture the response body, separate from the HTTP delivery outcome.
        response_preview:
          type:
            - string
            - "null"
        response_truncated:
          type: boolean
    WebhookDeliveryResponse:
      type: object
      description: |-
        One past delivery attempt for a webhook subscription.

        **Payload bodies are deliberately not included here.** A delivery's
        request and response bodies can carry document extraction content — the
        same `results` payload `WebhookEvent` embeds — which is exactly the class
        of data `WEBHOOK_RESULTS_MAX_BYTES` already treats as too sensitive/large
        to always inline. A list endpoint that paginates delivery *metadata*
        should not also be the place a caller incidentally pages through
        potentially large document content: that argues for a separate
        `GET .../deliveries/{delivery_id}` detail endpoint fetched on demand (not
        declared by this pass — only the list and retry operations are), the same
        "small by default, fetch the rest separately" shape the job-result and
        webhook-event payloads already use.
      required:
        - id
        - event_id
        - event_type
        - job_id
        - attempt_number
        - success
        - duration_ms
        - occurred_at
      properties:
        attempt_number:
          type: integer
          format: int64
          description: |-
            1-based attempt number within the redelivery sequence for this event
            Automatic retries follow the configured delivery limit.
        completed_at:
          type:
            - string
            - "null"
          description: Completion time; absent if the sender was interrupted before recording an outcome.
        duration_ms:
          type: integer
          format: int64
          description: How long the delivery attempt took, in milliseconds.
        error:
          type:
            - string
            - "null"
          description: Error message, present when the attempt did not succeed.
        event_id:
          type: string
          format: uuid
          description: Stable event identifier shared by automatic redeliveries.
        event_type:
          type: string
          description: The event type delivered (e.g. `"job.completed"`, `"job.failed"`).
        id:
          type: string
          format: uuid
          description: Unique identifier of this delivery attempt.
        job_id:
          type: string
          description: The job this delivery attempt carries the event for.
        occurred_at:
          type: string
          description: ISO 8601 timestamp the attempt occurred at.
        status_code:
          type:
            - integer
            - "null"
          format: int32
          description: |-
            HTTP status code returned by the endpoint, when a response was
            received at all (absent on a connection/timeout failure).
          minimum: 0
        success:
          type: boolean
          description: Whether the endpoint accepted the delivery.
    WebhookDeliveryStatus:
      type: string
      description: Current outcome of an inline webhook configured for a job.
      enum:
        - pending
        - delivered
        - failed
    WebhookEvent:
      type: object
      description: |-
        Webhook event payload delivered to customer endpoints when a job reaches
        a terminal status.

        This is the exact JSON body sent via HTTP POST to the webhook URL. When a
        secret is configured the request carries an `X-Webhook-Signature` header
        with the value `t=<unix seconds>,v1=<hex HMAC-SHA256>` over
        `"<t>.<body>"`; reject a delivery whose `t` is older than five minutes.

        `results` is present only on `job.completed`, and only when the extraction
        result fits in the body (100 KiB); otherwise fetch it from the job endpoint.
        `error` is present only on `job.failed`.
      required:
        - event
        - event_id
        - job_id
        - timestamp
      properties:
        error:
          $ref: '#/components/schemas/WebhookEventError'
          description: Error details, present only on `job.failed`.
        event:
          $ref: '#/components/schemas/WebhookEventType'
          description: Which terminal status the job reached.
        event_id:
          type: string
          format: uuid
          description: |-
            Stable identity of this event. Every delivery attempt of one event
            carries the same id, so a receiver deduplicates on it.
        job_id:
          type: string
          description: |-
            The job that reached the terminal status. UUID for an extraction job,
            ULID for a RAG ingest/reindex/migrate job.
        metadata:
          type:
            - object
            - "null"
          description: |-
            Metadata from the webhook configuration, echoed back for correlation.
            Absent when the configuration carried none. Sorted, so the signed body
            is byte-identical on every delivery attempt.
          additionalProperties:
            type: string
          propertyNames:
            type: string
        results:
          type: object
          description: |-
            The extraction result, present only on `job.completed` and only when it
            fits in the body. The shape is the `result` of `GET /v1/jobs/{id}`.
        timestamp:
          type: string
          format: date-time
          description: RFC 3339 time at which the job reached the terminal status.
    WebhookEventError:
      type: object
      description: Error information included in webhook failure events.
      required:
        - code
        - message
      properties:
        code:
          type: string
          description: Error code naming the failure cause (for example `EXTRACTION_ERROR`).
        message:
          type: string
          description: Human-readable error message.
    WebhookEventType:
      type: string
      description: |-
        The terminal job events a webhook can carry. The serialized names appear
        in the body's `event` field and in subscription filters where supported.
      enum:
        - job.completed
        - job.failed
        - job.cancelled
    XmlMetadata:
      type: object
      description: |-
        XML metadata extracted during XML parsing.

        Provides statistics about XML document structure.
      required:
        - element_count
        - unique_elements
        - format_type
      properties:
        element_count:
          type: integer
          format: int32
          description: Total number of XML elements processed
          minimum: 0
        format_type:
          type: string
          enum:
            - xml
        unique_elements:
          type: array
          items:
            type: string
          description: List of unique element tag names (sorted)
    YakeParams:
      type: object
      description: YAKE-specific parameters.
      required:
        - window_size
      properties:
        window_size:
          type: integer
          description: |-
            Window size for co-occurrence analysis (default: 2).

            Controls the context window for computing co-occurrence statistics.
          minimum: 0
      additionalProperties: false
    YearRange:
      type: object
      description: Year range for bibliographic metadata.
      properties:
        max:
          type:
            - integer
            - "null"
          format: int32
          description: Latest (maximum) year in the range.
          minimum: 0
        min:
          type:
            - integer
            - "null"
          format: int32
          description: Earliest (minimum) year in the range.
          minimum: 0
        years:
          type: array
          items:
            type: integer
            format: int32
            minimum: 0
          description: All individual years present in the collection.
  securitySchemes:
    bearer_auth:
      type: http
      scheme: bearer
security:
  - bearer_auth: []
tags:
  - name: extract
    description: Document extraction endpoints
  - name: jobs
    description: Job status endpoints
  - name: audit
    description: Customer-facing audit log endpoints
  - name: presets
    description: Preset registry endpoints
  - name: uploads
    description: Presigned upload endpoints
  - name: usage
    description: Usage and billing endpoints
  - name: extractions
    description: Structured extraction event endpoints
  - name: enrich
    description: 'Asynchronous text enrichment: keywords, entities, labels'
  - name: saved-presets
    description: User-created extraction schema preset CRUD
  - name: health
    description: Health check endpoints
  - name: rag
    description: RAG ingest, document lifecycle, and collection migration endpoints
  - name: auto-tune
    description: Auto-tune job submission, polling, and tuning-profile lifecycle endpoints
