openapi: 3.0.3
info:
  title: ChunkChef API
  version: 1.0.0
  contact:
    name: ChunkChef support
    url: https://hotdoc.io
  license:
    name: LicenseRef-Proprietary
    url: https://hotdoc.io/terms
  description: |
    Document-processing API.

    Asynchronous by design: POST /v1/jobs returns immediately with status
    JOB_STATUS_NEW. Poll GET /v1/jobs/{jobId} until a terminal status —
    JOB_STATUS_COMPLETE, JOB_STATUS_PARTIAL or JOB_STATUS_FAILED — then read
    GET /v1/jobs/{jobId}/result. Reading the result before completion
    returns partial rows, not an error.

    Response shape notes: all scalar fields are always present (empty string
    or empty array rather than omitted); message-typed fields may be
    explicitly null; fields declared `optional` in the schema are absent
    when unset. Proto-derived 64-bit integers are JSON strings; the
    `size_bytes` field of POST /v1/jobs/upload is a JSON number.

    Limits: uploads are capped at 20 MiB; a job accepts at most 20
    sourceUrls; each prompt is at most 65536 bytes; idempotencyKey is at
    most 255 characters. Exceeding a limit returns 400 or 429 — never 413.

    429 carries no Retry-After header; use client-side backoff.
  x-proto-revision: e892ddd7d0678039b183126ea1eb03cd5b8c9119
tags:
  - name: JobAPI
    description: |
      Create document-processing jobs, upload their inputs and read their results.
paths:
  /v1/jobs:
    get:
      summary: ListJobs lists jobs owned by the calling api-key's account.
      operationId: ListJobs
      responses:
        "200":
          description: A successful response.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ListJobsResponse"
        "400":
          description: Invalid argument, including a pageToken whose anchor job no longer resolves for this caller — it fell out of retention, or the token was not issued to this account — restart the listing with pageToken unset rather than retrying the same token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Status"
        "401":
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Status"
        "403":
          description: Access denied
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Status"
        "429":
          description: Rate limit. No Retry-After.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Status"
        "500":
          description: Internal error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Status"
        default:
          description: An unexpected error response.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Status"
      parameters:
        - name: statusEq
          description: |-
            status_eq optionally restricts the listing to a single status.

             - JOB_STATUS_FAILED: JOB_STATUS_FAILED — terminal: aborted with the job-level error.
             - JOB_STATUS_PARTIAL: JOB_STATUS_PARTIAL — terminal: OCR finished with a mix of done and failed files.
             - JOB_STATUS_LLM: JOB_STATUS_LLM — LLM stage running over OCR output.
          in: query
          required: false
          schema:
            type: string
            enum:
              - JOB_STATUS_UNSPECIFIED
              - JOB_STATUS_NEW
              - JOB_STATUS_FILE_PROCESSING
              - JOB_STATUS_OCR
              - JOB_STATUS_COMPLETE
              - JOB_STATUS_FAILED
              - JOB_STATUS_PARTIAL
              - JOB_STATUS_LLM
        - name: pageSize
          description: Defaults to 50, capped at 200.
          in: query
          required: false
          schema:
            type: integer
            format: int32
            default: 50
            maximum: 200
        - name: pageToken
          description: |
            page_token continues a previous page (opaque server-side token). The token anchors on a specific job, and this call returns 400 instead of a page whenever that anchor no longer resolves for the caller — either the anchor job fell out of retention, or the token was not issued to this account (a token is only ever valid for the account that received it). Narrowing or widening the other filters between pages does not invalidate the token; it only changes which rows follow the anchor. Treat that 400 as "the cursor no longer applies, restart the listing from pageToken unset", not as a malformed request.
          in: query
          required: false
          schema:
            type: string
        - name: X-Trace-Id
          in: header
          required: false
          schema:
            type: string
          description: |
            Optional client-supplied trace id, forwarded into server logs. Nothing is echoed back in response headers.
        - name: X-Request-Id
          in: header
          required: false
          schema:
            type: string
          description: |
            Alternative to X-Trace-Id; forwarded the same way and likewise not echoed back.
      tags:
        - JobAPI
      description: |
        Returns jobs in `created DESC, id ASC` order (newest first); the cursor semantics of `pageToken` depend on that order staying fixed.
    post:
      summary: CreateJob enqueues a new document-processing job.
      operationId: CreateJob
      responses:
        "200":
          description: A successful response.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreateJobResponse"
        "400":
          description: Invalid argument, including an idempotency key reused with different parameters
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Status"
        "401":
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Status"
        "403":
          description: Access denied
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Status"
        "429":
          description: Monthly job quota (carries QuotaExceededDetail in details), concurrency cap ("too many concurrent jobs"), or per-principal rate limit. No Retry-After.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Status"
        "500":
          description: Internal error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Status"
        default:
          description: An unexpected error response.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Status"
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateJobRequest"
        description: CreateJobRequest enqueues a job over a set of source URLs.
        required: true
      tags:
        - JobAPI
      parameters:
        - name: X-Trace-Id
          in: header
          required: false
          schema:
            type: string
          description: |
            Optional client-supplied trace id, forwarded into server logs. Nothing is echoed back in response headers.
        - name: X-Request-Id
          in: header
          required: false
          schema:
            type: string
          description: |
            Alternative to X-Trace-Id; forwarded the same way and likewise not echoed back.
  /v1/jobs/{jobId}:
    get:
      summary: GetJob returns the job header (status, errors, file list) without OCR results.
      operationId: GetJob
      responses:
        "200":
          description: A successful response.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GetJobResponse"
        "400":
          description: Invalid argument
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Status"
        "401":
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Status"
        "403":
          description: Access denied
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Status"
        "404":
          description: No such job for this account
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Status"
        "429":
          description: Rate limit. No Retry-After.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Status"
        "500":
          description: Internal error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Status"
        default:
          description: An unexpected error response.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Status"
      parameters:
        - name: jobId
          in: path
          required: true
          schema:
            type: string
        - name: X-Trace-Id
          in: header
          required: false
          schema:
            type: string
          description: |
            Optional client-supplied trace id, forwarded into server logs. Nothing is echoed back in response headers.
        - name: X-Request-Id
          in: header
          required: false
          schema:
            type: string
          description: |
            Alternative to X-Trace-Id; forwarded the same way and likewise not echoed back.
      tags:
        - JobAPI
  /v1/jobs/{jobId}/result:
    get:
      summary: GetJobResult returns the job header plus all per-file OCR rows.
      operationId: GetJobResult
      responses:
        "200":
          description: A successful response.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GetJobResultResponse"
        "400":
          description: Invalid argument
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Status"
        "401":
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Status"
        "403":
          description: Access denied
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Status"
        "404":
          description: No such job for this account
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Status"
        "429":
          description: Rate limit. No Retry-After.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Status"
        "500":
          description: Internal error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Status"
        default:
          description: An unexpected error response.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Status"
      parameters:
        - name: jobId
          in: path
          required: true
          schema:
            type: string
        - name: X-Trace-Id
          in: header
          required: false
          schema:
            type: string
          description: |
            Optional client-supplied trace id, forwarded into server logs. Nothing is echoed back in response headers.
        - name: X-Request-Id
          in: header
          required: false
          schema:
            type: string
          description: |
            Alternative to X-Trace-Id; forwarded the same way and likewise not echoed back.
      tags:
        - JobAPI
      description: |
        Returns per-file OCR text and model answers. Safe to call before the job reaches a terminal status — it returns the rows finished so far rather than an error.
  /v1/jobs/upload:
    post:
      operationId: Upload
      tags:
        - JobAPI
      summary: Upload one file and receive a URL usable as a sourceUrl
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - file
              properties:
                file:
                  type: string
                  format: binary
      responses:
        "200":
          description: Stored
          content:
            application/json:
              schema:
                type: object
                properties:
                  url:
                    type: string
                  name:
                    type: string
                    description: |
                      Server-sanitized filename; may differ from the submitted one (a .bin extension is appended when the name has none).
                  size_bytes:
                    type: integer
                    format: int64
                    description: A JSON number, unlike proto-derived int64 fields.
        "400":
          description: Not multipart, missing file part, or empty file
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UploadError"
        "401":
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UploadError"
        "429":
          description: Job quota exhausted, file larger than 20 MiB, or per-principal rate limit. No Retry-After.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UploadError"
        "500":
          description: Internal error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UploadError"
        default:
          description: Unexpected error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UploadError"
components:
  schemas:
    Any:
      type: object
      properties:
        "@type":
          type: string
      additionalProperties: {}
    ChunkRef:
      type: object
      properties:
        file:
          type: string
          description: file is the S3 URL of the source file ("" at scope=job aggregate rows).
        chunk:
          type: integer
          format: int64
          description: chunk is the 0-based chunk index within the file.
      description: ChunkRef identifies one chunk by its source file and 0-based index.
    ConsensusMode:
      type: string
      enum:
        - CONSENSUS_MODE_UNSPECIFIED
        - CONSENSUS_MODE_TWO_RUNS
        - CONSENSUS_MODE_THREE_RUNS
        - CONSENSUS_MODE_FIVE_RUNS
      description: |-
        ConsensusMode selects how many independent LLM runs vote per chunk
        (schema jobs only). The run count is part of the name: it is the
        caller's token-cost multiplier and is never retuned under an existing
        name.

         - CONSENSUS_MODE_UNSPECIFIED: consensus disabled (default)
         - CONSENSUS_MODE_TWO_RUNS: 2 runs - cheap instability probe
         - CONSENSUS_MODE_THREE_RUNS: 3 runs - recommended default vote
         - CONSENSUS_MODE_FIVE_RUNS: 5 runs - maximum scrutiny
    ConsensusOptions:
      type: object
      properties:
        mode:
          $ref: "#/components/schemas/ConsensusMode"
      description: |
        Optional k-run consensus voting. Requires `responseSchema` on the same request; without it the job is rejected with 400.
    ConsensusResult:
      type: object
      properties:
        k:
          type: integer
          format: int64
          title: runs requested (2|3|5)
        runsVotable:
          type: integer
          format: int64
          title: runs that produced a votable JSON answer
        minAgreement:
          type: number
          format: double
          description: |-
            Minimum per-field agreement across all voted fields and array
            elements, each as a fraction of its per-path denominator
            (runs_votable at top level, the container-provider count inside
            recursions). 0 when no vote happened or the vote degraded; 1.0 when
            a vote happened but produced zero voted fields.
        incomplete:
          type: boolean
          title: runs_votable < k, or vote degraded
        disagreements:
          type: array
          items:
            $ref: "#/components/schemas/Disagreement"
        disagreementsDropped:
          type: integer
          format: int64
      description: ConsensusResult is the per-chunk outcome of multi-run LLM voting.
    CreateJobRequest:
      type: object
      properties:
        sourceUrls:
          type: array
          items:
            type: string
          description: |
            source_urls is the list of URLs to download and process. At most 20 entries per job.
        title:
          type: string
          description: title is an optional human-readable name.
        metadata:
          type: object
          additionalProperties:
            type: string
          description: metadata is arbitrary caller-supplied key/value tags.
        prompts:
          type: array
          items:
            type: string
          description: prompts is the list of LLM prompts to run per extracted file.
        neural:
          allOf:
            - $ref: "#/components/schemas/NeuralConfig"
            - description: neural is the inline BYOK neural config; required (validated server-side).
        ocr:
          allOf:
            - $ref: "#/components/schemas/OCRConfig"
            - description: ocr is the inline BYOK OCR config; required (validated server-side).
        webhookUrl:
          type: string
          description: |-
            webhook_url is an optional absolute http/https endpoint POSTed once when
            the job reaches a terminal status. Empty = no webhook.
        webhookSecret:
          type: string
          description: |-
            webhook_secret is an optional HMAC-SHA256 signing secret. Input only,
            never echoed. If empty, the webhook is delivered without a signature.
        idempotencyKey:
          type: string
          description: |
            idempotency_key is an optional client-supplied dedup key (opaque, at most 255 characters) — a body field, not an Idempotency-Key header. A retried CreateJob with the same key returns the same job instead of creating a duplicate; reusing a key with a different request body is rejected with 400. The key is honored for the job's retention window; after the job is reaped a replay creates a new job. Input only.
        extractionMode:
          allOf:
            - $ref: "#/components/schemas/ExtractionMode"
            - description: |-
                extraction_mode selects the text-extraction strategy. UNSPECIFIED (default)
                inherits the account's flow-execution default, which itself falls back to
                HYBRID.
        responseSchema:
          type: string
          description: |
            response_schema is an optional JSON Schema (raw string) constraining the LLM answer's shape. Empty = off. Must be self-contained (internal #/$defs only, no external $ref). Validated at CreateJob; a schema larger than 128 KiB, nested deeper than 64 levels, containing more than 10000 nodes, or referencing anything external is rejected with 400.
        merge:
          allOf:
            - $ref: "#/components/schemas/MergeOptions"
            - description: |-
                merge configures the per-job merge (reduce) of per-chunk LLM answers into one
                whole-document result. Disabled by default.
        consensus:
          allOf:
            - $ref: "#/components/schemas/ConsensusOptions"
            - description: |-
                consensus configures multi-run LLM voting per chunk (schema jobs only).
                Disabled by default.
      description: CreateJobRequest enqueues a job over a set of source URLs.
      required:
        - sourceUrls
        - neural
        - ocr
    CreateJobResponse:
      type: object
      properties:
        job:
          allOf:
            - $ref: "#/components/schemas/Job"
            - description: job is the persisted job in NEW state.
      description: CreateJobResponse echoes the freshly-created Job (status NEW).
    Disagreement:
      type: object
      properties:
        fieldPath:
          type: string
          description: |-
            JSON dot-path of the disputed field, or of the ARRAY for element
            disputes (element membership; indices never appear). "" = root.
        variants:
          type: array
          items:
            $ref: "#/components/schemas/DisagreementVariant"
        variantsDropped:
          type: integer
          format: int64
          description: Variants dropped by the per-disagreement cap.
      description: Disagreement records one field or array where votable runs disagreed.
    DisagreementVariant:
      type: object
      properties:
        value:
          type: string
          description: |-
            Compact JSON of the first-seen original value, truncated rune-safe
            to 512 bytes (may be non-JSON after truncation).
        runs:
          type: integer
          format: int64
          description: How many votable runs produced this value/element.
        included:
          type: boolean
          description: |-
            included = this variant WON the vote (scalar winner / agreed-array
            member / winning side of a presence vote). Decoupled from textual
            presence: a winning null variant carries included: true even though
            its key is omitted from the agreed answer.
      description: |-
        DisagreementVariant is one competing value/element observed for a
        disputed field.
    ExtractionMode:
      type: string
      enum:
        - EXTRACTION_MODE_UNSPECIFIED
        - EXTRACTION_MODE_HYBRID
        - EXTRACTION_MODE_OCR_ALWAYS
      description: |-
        ExtractionMode selects how a job extracts text from its files.

         - EXTRACTION_MODE_UNSPECIFIED: EXTRACTION_MODE_UNSPECIFIED inherits the next level down: in
        CreateJobRequest -> the account default; in the account default -> the
        system default (HYBRID).
         - EXTRACTION_MODE_HYBRID: EXTRACTION_MODE_HYBRID lets the converter decide text-extract vs OCR
        (the default heuristic behaviour).
         - EXTRACTION_MODE_OCR_ALWAYS: EXTRACTION_MODE_OCR_ALWAYS forces the neural OCR path, bypassing the
        converter heuristic.
    GetJobResponse:
      type: object
      properties:
        job:
          $ref: "#/components/schemas/Job"
      description: GetJobResponse wraps the Job payload returned by GetJob.
    GetJobResultResponse:
      type: object
      properties:
        result:
          $ref: "#/components/schemas/JobResult"
      description: GetJobResultResponse wraps the JobResult payload returned by GetJobResult.
    Job:
      type: object
      properties:
        id:
          type: string
        accountId:
          type: string
        title:
          type: string
        metadata:
          type: object
          additionalProperties:
            type: string
        status:
          $ref: "#/components/schemas/JobStatus"
        error:
          type: string
          description: error captures a terminal failure message, if any.
        sourceUrls:
          type: array
          items:
            type: string
          description: source_urls is the original list of URLs from CreateJob.
        files:
          type: array
          items:
            type: string
          description: files is the list of S3 URLs of unpacked files (filled by file_processing).
        created:
          type: string
          format: date-time
        updated:
          type: string
          format: date-time
        prompts:
          type: array
          items:
            type: string
          description: prompts is the list of LLM prompts captured at job creation.
        neural:
          allOf:
            - $ref: "#/components/schemas/JobNeural"
            - description: |-
                neural is the response-safe neural config (type/model/reasoning_effort);
                never carries the api key.
        extractionMode:
          allOf:
            - $ref: "#/components/schemas/ExtractionMode"
            - description: |-
                extraction_mode is the mode resolved at job creation (never UNSPECIFIED on a
                persisted job): request -> account default -> HYBRID.
        responseSchema:
          type: string
          description: response_schema echoes the schema the job was created with (empty if none).
        merge:
          allOf:
            - $ref: "#/components/schemas/MergeOptions"
            - description: merge echoes the resolved merge configuration the job was created with.
        consensus:
          allOf:
            - $ref: "#/components/schemas/ConsensusOptions"
            - description: consensus echoes the resolved consensus configuration the job was created with.
      description: Job is the header view of a document-processing job.
    JobLLMData:
      type: object
      properties:
        jobId:
          type: string
        file:
          type: string
          description: file is the S3 URL of the source file the chunk was extracted from.
        promptIndex:
          type: integer
          format: int64
          description: prompt_index is the 0-based index into Job.prompts.
        chunkIndex:
          type: integer
          format: int64
          description: chunk_index is the 0-based chunk number within (file, prompt_index).
        chunkTotal:
          type: integer
          format: int64
          description: chunk_total is the total number of chunks for (file, prompt_index).
        status:
          $ref: "#/components/schemas/JobLLMStatus"
        skipReason:
          type: string
          description: skip_reason explains why a SKIPPED row was skipped.
        model:
          type: string
          description: model is the LLM-reported model identifier used for the call.
        request:
          type: string
          description: request carries the JSON-encoded LLM request payload.
        rawData:
          type: string
          description: raw_data carries the raw JSON response from the LLM, for debugging.
        content:
          type: string
          description: content is the extracted assistant message content.
        error:
          type: string
          description: error is the per-chunk error message, if any.
        duration:
          type: string
          format: int64
          description: duration is the LLM call wall-clock duration in nanoseconds (Go time.Duration).
        created:
          type: string
          format: date-time
        updated:
          type: string
          format: date-time
        schemaValid:
          type: boolean
          description: |-
            schema_valid: does this row's content conform to the job's response_schema?
            Present only for single-chunk schema jobs; unset otherwise.
        schemaErrors:
          type: array
          items:
            type: string
          description: "schema_errors: conformance violations (<=10, each <=512 chars) when invalid."
        consensus:
          allOf:
            - $ref: "#/components/schemas/ConsensusResult"
            - description: |-
                consensus is the multi-run voting outcome for this row; unset when the
                job has no consensus.
      description: JobLLMData is the per-prompt-per-chunk LLM record.
    JobLLMStatus:
      type: string
      enum:
        - JOB_LLM_STATUS_UNSPECIFIED
        - JOB_LLM_STATUS_PENDING
        - JOB_LLM_STATUS_SKIPPED
        - JOB_LLM_STATUS_DONE
        - JOB_LLM_STATUS_FAILED
      description: |-
        JobLLMStatus enumerates the per-prompt-per-chunk LLM states.

         - JOB_LLM_STATUS_SKIPPED: JOB_LLM_STATUS_SKIPPED — chunk was skipped (e.g. empty OCR content).
         - JOB_LLM_STATUS_FAILED: JOB_LLM_STATUS_FAILED — exhausted retries or hit a permanent error.
    JobNeural:
      type: object
      properties:
        type:
          $ref: "#/components/schemas/NeuralClientType"
        model:
          type: string
        reasoningEffort:
          type: string
        chunkBudgetTokens:
          type: integer
          format: int64
          description: |-
            chunk_budget_tokens is the resolved effective whole-prompt budget persisted
            at creation (gross minus safetyMargin); always > 0 in a stored job.
      description: |-
        JobNeural is the response-safe view of a job's neural config: it carries
        NO api_key / ciphertext / nonce / kek_version field by construction.
    JobOCRData:
      type: object
      properties:
        jobId:
          type: string
        file:
          type: string
          description: file is the S3 URL of the file processed by OCR.
        status:
          $ref: "#/components/schemas/JobOCRStatus"
        request:
          type: string
          description: request carries the JSON-encoded converter request payload.
        rawData:
          type: string
          description: raw_data carries the raw JSON response from the converter, for debugging.
        content:
          type: string
          description: content is the extracted text (empty for SKIPPED).
        model:
          type: string
          description: model is the converter-reported extraction method.
        error:
          type: string
          description: error is the per-file error message, if any.
        duration:
          type: string
          format: int64
          description: duration is the OCR wall-clock duration in nanoseconds (Go time.Duration).
        created:
          type: string
          format: date-time
        updated:
          type: string
          format: date-time
        outputFormat:
          type: string
          description: |-
            output_format is the converter output format for this file
            (markdown|html|xml|plain); drives the LLM-stage chunker.
      description: JobOCRData is the per-unpacked-file OCR record.
    JobOCRStatus:
      type: string
      enum:
        - JOB_OCR_STATUS_UNSPECIFIED
        - JOB_OCR_STATUS_PENDING
        - JOB_OCR_STATUS_SKIPPED
        - JOB_OCR_STATUS_DONE
        - JOB_OCR_STATUS_FAILED
      description: |-
        JobOCRStatus enumerates the per-file OCR states.

         - JOB_OCR_STATUS_SKIPPED: JOB_OCR_STATUS_SKIPPED — extension was outside the OCR whitelist.
         - JOB_OCR_STATUS_FAILED: JOB_OCR_STATUS_FAILED — exhausted retries or hit a permanent error.
    JobResult:
      type: object
      properties:
        job:
          allOf:
            - $ref: "#/components/schemas/Job"
            - description: job is the job header.
        ocr:
          type: array
          items:
            $ref: "#/components/schemas/JobOCRData"
          description: ocr is the list of per-file OCR records.
        llm:
          type: array
          items:
            $ref: "#/components/schemas/JobLLMData"
          description: llm is the list of per-prompt-per-chunk LLM records.
        merged:
          type: array
          items:
            $ref: "#/components/schemas/MergedResult"
          description: |-
            merged is the list of whole-document merged results (empty unless merge is
            enabled): one per file at scope=file, one with empty file at scope=job.
      description: JobResult bundles the header with all per-file OCR rows.
    JobStatus:
      type: string
      enum:
        - JOB_STATUS_UNSPECIFIED
        - JOB_STATUS_NEW
        - JOB_STATUS_FILE_PROCESSING
        - JOB_STATUS_OCR
        - JOB_STATUS_COMPLETE
        - JOB_STATUS_FAILED
        - JOB_STATUS_PARTIAL
        - JOB_STATUS_LLM
      description: |-
        JobStatus enumerates the overall states of a Job.

         - JOB_STATUS_FAILED: JOB_STATUS_FAILED — terminal: aborted with the job-level error.
         - JOB_STATUS_PARTIAL: JOB_STATUS_PARTIAL — terminal: OCR finished with a mix of done and failed files.
         - JOB_STATUS_LLM: JOB_STATUS_LLM — LLM stage running over OCR output.
    ListJobsResponse:
      type: object
      properties:
        jobs:
          type: array
          items:
            $ref: "#/components/schemas/Job"
          description: jobs is the page contents.
        nextPageToken:
          type: string
          description: next_page_token, if non-empty, can be used to fetch the next page.
      description: ListJobsResponse returns one page of jobs.
    MergeConflict:
      type: object
      properties:
        field:
          type: string
          description: field is the JSON path / element path of the conflicting field.
        values:
          type: array
          items:
            type: string
          description: values are the competing non-null values (clamped for size).
        sources:
          type: array
          items:
            $ref: "#/components/schemas/ChunkRef"
          description: sources are the chunks the competing values came from.
      description: MergeConflict records one field where chunks disagreed on a non-null value.
    MergeConflictPolicy:
      type: string
      enum:
        - MERGE_CONFLICT_POLICY_UNSPECIFIED
        - MERGE_CONFLICT_POLICY_FIRST_NON_NULL
        - MERGE_CONFLICT_POLICY_MAJORITY
      description: |-
        MergeConflictPolicy selects how conflicting scalar values are resolved.

         - MERGE_CONFLICT_POLICY_FIRST_NON_NULL: MERGE_CONFLICT_POLICY_FIRST_NON_NULL — first non-null value by chunk order (default).
         - MERGE_CONFLICT_POLICY_MAJORITY: MERGE_CONFLICT_POLICY_MAJORITY — most frequent non-null value (>=3 chunks; else first-non-null).
    MergeFormat:
      type: string
      enum:
        - MERGE_FORMAT_UNSPECIFIED
        - MERGE_FORMAT_AUTO
        - MERGE_FORMAT_JSON
        - MERGE_FORMAT_XML
        - MERGE_FORMAT_HTML
        - MERGE_FORMAT_MARKDOWN
        - MERGE_FORMAT_TEXT
      description: |-
        MergeFormat forces the merge strategy for the answer format; AUTO sniffs it.

         - MERGE_FORMAT_AUTO: MERGE_FORMAT_AUTO — detect the answer format from content (default).
         - MERGE_FORMAT_JSON: MERGE_FORMAT_JSON — structural JSON key-union merge.
         - MERGE_FORMAT_XML: MERGE_FORMAT_XML — XML tree concat + fusion.
         - MERGE_FORMAT_HTML: MERGE_FORMAT_HTML — HTML tree concat + fusion.
         - MERGE_FORMAT_MARKDOWN: MERGE_FORMAT_MARKDOWN — markdown block merge + GFM table fusion.
         - MERGE_FORMAT_TEXT: MERGE_FORMAT_TEXT — plain-text ordered concat.
    MergeOptions:
      type: object
      properties:
        enabled:
          type: boolean
          description: enabled turns the merge reduce on; when false all other fields are ignored.
        scope:
          allOf:
            - $ref: "#/components/schemas/MergeScope"
            - description: scope selects per-file vs whole-job merge granularity.
        conflictPolicy:
          allOf:
            - $ref: "#/components/schemas/MergeConflictPolicy"
            - description: conflict_policy selects how differing scalar values across chunks resolve.
        dedupeBy:
          type: array
          items:
            type: string
          description: |-
            dedupe_by lists JSON field names identifying duplicate records to collapse
            across files (scope=job, JSON+schema only). Empty = technical overlap dedup
            only (the safe default).
        format:
          allOf:
            - $ref: "#/components/schemas/MergeFormat"
            - description: format forces the answer format; UNSPECIFIED/AUTO sniffs it from content.
      description: |
        Optional server-side merge of per-chunk answers. Off unless `enabled` is true. `dedupeBy` requires `responseSchema` (400 otherwise) and accepts at most 32 fields.
    MergeScope:
      type: string
      enum:
        - MERGE_SCOPE_UNSPECIFIED
        - MERGE_SCOPE_FILE
        - MERGE_SCOPE_JOB
      description: |-
        MergeScope selects the granularity of the merge reduce.

         - MERGE_SCOPE_FILE: MERGE_SCOPE_FILE — one merged result per file (the default).
         - MERGE_SCOPE_JOB: MERGE_SCOPE_JOB — one merged result for the whole job (across files).
    MergedResult:
      type: object
      properties:
        file:
          type: string
          description: file is the S3 URL this result merges ("" for a scope=job aggregate).
        format:
          type: string
          description: format is the resolved answer format of the merged content.
        content:
          type: string
          description: content is the merged whole-document result serialized in its format.
        schemaValid:
          type: boolean
          description: |-
            schema_valid reports strict-schema conformance of the merged object; unset
            when the job has no response_schema.
        conflicts:
          type: array
          items:
            $ref: "#/components/schemas/MergeConflict"
          description: conflicts lists fields where chunks disagreed on a non-null value (clamped).
        dedupeRemovedTechnical:
          type: integer
          format: int64
          description: dedupe_removed_technical counts overlap duplicates removed automatically.
        dedupeRemovedContent:
          type: integer
          format: int64
          description: dedupe_removed_content counts cross-file duplicates removed via dedupe_by.
        mergeIncomplete:
          type: boolean
          description: merge_incomplete is true when some chunks failed or degraded to concat.
        incompleteChunks:
          type: array
          items:
            $ref: "#/components/schemas/ChunkRef"
          description: incomplete_chunks lists the failed/degraded chunks when merge_incomplete.
      description: |-
        MergedResult is one whole-document merged result: one per file at scope=file,
        one with an empty file at scope=job.
    NeuralClientType:
      type: string
      enum:
        - NEURAL_CLIENT_TYPE_UNSPECIFIED
        - NEURAL_CLIENT_TYPE_GROK
        - NEURAL_CLIENT_TYPE_OPENAI
        - NEURAL_CLIENT_TYPE_CLAUDE
        - NEURAL_CLIENT_TYPE_TOGETHER
        - NEURAL_CLIENT_TYPE_OPENROUTER
        - NEURAL_CLIENT_TYPE_XIAOMI
        - NEURAL_CLIENT_TYPE_DEEPSEEK
        - NEURAL_CLIENT_TYPE_MISTRAL
      description: |
        Provider selector. As an input on `neural.type` and `ocr.provider` this field is required and NEURAL_CLIENT_TYPE_UNSPECIFIED is rejected with 400 — unlike ExtractionMode, MergeScope, MergeFormat, MergeConflictPolicy and ConsensusMode, where *_UNSPECIFIED legitimately means "use the server default".
    NeuralConfig:
      type: object
      properties:
        type:
          $ref: "#/components/schemas/NeuralClientType"
        model:
          type: string
          description: model is non-empty and otherwise opaque (forwarded to the provider as-is).
        apiKey:
          type: string
          description: "api_key is a secret: input only, never echoed in any response message."
        reasoningEffort:
          type: string
          description: reasoning_effort is advisory; providers that do not support it ignore it.
        chunkBudgetTokens:
          type: integer
          format: int64
          description: |
            chunk_budget_tokens is the client-declared gross per-chunk token budget (prompt + content) for the LLM stage. 0 = unset, meaning the server uses its config default. At CreateJob the server subtracts safetyMargin and persists the resulting effective budget. The client (BYOK) knows its model's context window; the server cannot. When non-zero it must fall between 16000 and 2000000, otherwise the request is rejected with 400.
      description: NeuralConfig is the inline BYOK neural configuration supplied on CreateJob.
      required:
        - type
        - model
        - apiKey
    OCRConfig:
      type: object
      properties:
        provider:
          allOf:
            - $ref: "#/components/schemas/NeuralClientType"
            - description: provider selects the OCR/vision backend. Required.
        model:
          type: string
          description: |-
            model is the provider model id (e.g. "mistral-ocr-latest", "gpt-4o").
            Required; forwarded opaque.
        providerKey:
          type: string
          description: "provider_key is a secret: input only, never echoed in any response message."
      description: |-
        OCRConfig is the inline BYOK OCR configuration supplied on CreateJob.
        provider/model select the vision backend; provider_key is forwarded to the
        converter's OCR backend as the x-provider-key header. Extensible: future
        per-request OCR parameters are added as new fields here. Required (validated
        server-side).
      required:
        - provider
        - model
        - providerKey
    Status:
      type: object
      properties:
        code:
          type: integer
          format: int32
        message:
          type: string
        details:
          type: array
          items:
            $ref: "#/components/schemas/Any"
    UploadError:
      type: object
      description: |
        Error envelope for POST /v1/jobs/upload. Unlike the gateway envelope it carries no `details` array.
      properties:
        code:
          type: integer
          format: int32
          description: Numeric gRPC status code
        message:
          type: string
        reason:
          type: string
          description: |
            Reserved for future use. The public upload handler (server/upload/handler.go) never populates this field today — the key is omitted from the response body entirely (not sent empty), so a client must not compare it against "".
    QuotaExceededDetail:
      type: object
      description: |
        Typed detail attached to a monthly-quota 429. Appears inside Status.details with "@type": "type.googleapis.com/chunkchef.v1.billing.QuotaExceededDetail".
      properties:
        jobsUsed:
          type: integer
          format: int64
        jobsLimit:
          type: integer
          format: int64
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: |
        An account API key or a Supabase-issued JWT, sent as `Authorization: Bearer <token>`.
servers:
  - url: https://api.hotdoc.io
    description: Production
  - url: https://api.hotdoc.run
    description: Stage
security:
  - bearerAuth: []
