openapi: 3.1.0
info:
  title: AI Code Review Server API
  version: 1.0.0
  description: >-
    Canonical API for submitting PHP and Drupal code reviews. Each review
    executes through a provider profile the calling site registered on the
    administrator-only control plane; a request never carries a provider
    credential, upstream, header, route, or raw model identifier.
servers:
  - url: https://reviewer.example.com
    description: Deployment-specific server origin
paths:
  /healthz:
    get:
      operationId: getLiveness
      summary: Check whether the Drupal request path is alive
      security: []
      responses:
        '200':
          description: The server process is alive
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HealthStatus'
  /readyz:
    get:
      operationId: getReadiness
      summary: Check configured review traffic and enabled language sidecar readiness
      security: []
      responses:
        '200':
          description: The server is ready for review traffic
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HealthStatus'
        '503':
          description: The server or its enabled language sidecar is not ready for review traffic
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HealthStatus'
  /api/ai-code-review/v1/reviews:
    post:
      operationId: createReview
      summary: Submit a repository change for review
      description: >-
        Requires boolean service enablement in addition to authentication and
        route permission. Disabled service returns 503 service_unavailable
        before admission, idempotency acceptance, Job/delivery allocation or
        execution IO. The shared worker checks fresh active configuration at
        its fenced start decision before preparation and queued-to-running;
        disabled pending work retains its input, delivery and attempt count.
        Work already past that decision may prepare and finish under existing
        cancellation and deadline bounds. Status, result, cancellation and
        provider-management routes retain their ordinary authorization rules.
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/RequestId'
        - $ref: '#/components/parameters/IdempotencyKey'
        - $ref: '#/components/parameters/Prefer'
      requestBody:
        required: true
        description: >-
          Unencoded JSON with a maximum wire size of 1,048,576 bytes. A
          malformed or mismatched Content-Length is rejected before decode.
        x-maximum-body-bytes: 1048576
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReviewRequest'
      responses:
        '200':
          description: Synchronous review completed
          headers:
            X-Request-ID:
              $ref: '#/components/headers/RequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReviewResult'
        '202':
          description: Review submission accepted for asynchronous processing or returned for an idempotent retry
          headers:
            Location:
              description: Canonical status resource for the accepted review
              schema:
                type: string
                format: uri-reference
            Preference-Applied:
              description: Confirms asynchronous processing was selected
              schema:
                type: string
                const: respond-async
            X-Request-ID:
              $ref: '#/components/headers/RequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReviewJob'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '405':
          $ref: '#/components/responses/MethodNotAllowed'
        '406':
          $ref: '#/components/responses/NotAcceptable'
        '409':
          $ref: '#/components/responses/Conflict'
        '413':
          $ref: '#/components/responses/ContentTooLarge'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '422':
          $ref: '#/components/responses/UnprocessableContent'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
        '502':
          $ref: '#/components/responses/BadGateway'
        '503':
          $ref: '#/components/responses/Unavailable'
        '504':
          $ref: '#/components/responses/GatewayTimeout'
  /api/ai-code-review/v1/reviews/{reviewId}:
    get:
      operationId: getReviewStatus
      summary: Retrieve owner-scoped review lifecycle status
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/RequestId'
        - name: reviewId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Safe lifecycle metadata for the requested review
          headers:
            X-Request-ID:
              $ref: '#/components/headers/RequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReviewStatus'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: The review does not exist or is not owned by the caller
          headers:
            X-Request-ID:
              $ref: '#/components/headers/RequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '405':
          $ref: '#/components/responses/MethodNotAllowed'
        '406':
          $ref: '#/components/responses/NotAcceptable'
        '500':
          $ref: '#/components/responses/InternalError'
    delete:
      operationId: cancelReview
      summary: Cancel an owner-scoped queued or running review
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/RequestId'
        - name: reviewId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: The review is cancelled or was already cancelled
          headers:
            X-Request-ID:
              $ref: '#/components/headers/RequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReviewStatus'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: The review does not exist or is not owned by the caller
          headers:
            X-Request-ID:
              $ref: '#/components/headers/RequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '405':
          $ref: '#/components/responses/MethodNotAllowed'
        '406':
          $ref: '#/components/responses/NotAcceptable'
        '409':
          description: A non-cancelled terminal outcome won the lifecycle race
          headers:
            X-Request-ID:
              $ref: '#/components/headers/RequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Durable queue cleanup is temporarily unavailable and cancellation should be retried
          headers:
            X-Request-ID:
              $ref: '#/components/headers/RequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          $ref: '#/components/responses/InternalError'
  /api/ai-code-review/v1/reviews/{reviewId}/result:
    get:
      operationId: getReviewResult
      summary: Retrieve the structured result of a completed owner-scoped review
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/RequestId'
        - name: reviewId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Structured output for the requested completed review
          headers:
            X-Request-ID:
              $ref: '#/components/headers/RequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReviewResult'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: The review does not exist or is not owned by the caller
          headers:
            X-Request-ID:
              $ref: '#/components/headers/RequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '405':
          $ref: '#/components/responses/MethodNotAllowed'
        '406':
          $ref: '#/components/responses/NotAcceptable'
        '409':
          description: The review has not completed
          headers:
            X-Request-ID:
              $ref: '#/components/headers/RequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '410':
          description: The completed review result has passed its retention period
          headers:
            X-Request-ID:
              $ref: '#/components/headers/RequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          $ref: '#/components/responses/InternalError'
  /api/ai-code-review/v1/provider-profile-capabilities:
    get:
      operationId: getProviderProfileCapabilities
      summary: Discover provider-profile registration capabilities
      description: >-
        Returns the current registration public key and only the provider and
        execution-route combinations this deployment can execute. The
        response is administrator-only and must not be cached.
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/RequestId'
      responses:
        '200':
          description: The current non-secret registration capabilities
          headers:
            Cache-Control:
              description: Prevents capability and key caching
              schema:
                type: string
                const: no-store, private
            X-Request-ID:
              $ref: '#/components/headers/RequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProviderProfileCapabilities'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '405':
          $ref: '#/components/responses/MethodNotAllowed'
        '406':
          $ref: '#/components/responses/NotAcceptable'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ProviderProfileRegistrationUnavailable'
  /api/ai-code-review/v1/provider-profiles:
    post:
      operationId: createProviderProfile
      summary: Register a provider profile, delivering its credential once
      description: >-
        Requires the manage ai code review provider profiles permission on
        the account the service token maps to. The profile is bound to the
        calling client. An apisix_managed profile becomes active only after
        the APISIX provisioner durably acknowledged the secret; a
        direct_server credential is re-encrypted with a deployment key before
        it is stored.
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/RequestId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProviderProfileRegistration'
      responses:
        '201':
          description: The profile is active and may be named by reviews
          headers:
            X-Request-ID:
              $ref: '#/components/headers/RequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProviderProfile'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '405':
          $ref: '#/components/responses/MethodNotAllowed'
        '406':
          $ref: '#/components/responses/NotAcceptable'
        '409':
          description: >-
            The caller already holds its deployment-configured maximum of
            active profiles (error code provider_profile_limit_exceeded) or
            of retained registration operations (error code
            provider_profile_registration_limit); revoke one first. A
            registration_id reused with a different request, or one whose
            retained tombstone forbids reuse, returns
            provider_profile_registration_conflict, and a replay naming an
            already revoked profile returns
            provider_profile_registration_revoked. The sealed envelope was
            not consumed.
          headers:
            X-Request-ID:
              $ref: '#/components/headers/RequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '413':
          $ref: '#/components/responses/ContentTooLarge'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '422':
          $ref: '#/components/responses/UnprocessableContent'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ProvisionerUnavailable'
    get:
      operationId: listProviderProfiles
      summary: List the caller's provider profiles
      description: >-
        Returns the caller's newest profiles, active and revoked, up to a
        deployment-configured bound. Revoked profiles are retained for a
        deployment-configured period and then removed by cron.
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/RequestId'
      responses:
        '200':
          description: Non-secret metadata of the caller's newest profiles
          headers:
            X-Request-ID:
              $ref: '#/components/headers/RequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProviderProfileCollection'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '405':
          $ref: '#/components/responses/MethodNotAllowed'
        '406':
          $ref: '#/components/responses/NotAcceptable'
        '500':
          $ref: '#/components/responses/InternalError'
  /api/ai-code-review/v1/provider-profiles/{profileId}:
    get:
      operationId: getProviderProfile
      summary: Read one caller-owned provider profile
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/RequestId'
        - $ref: '#/components/parameters/ProfileId'
      responses:
        '200':
          description: Non-secret metadata of the requested profile
          headers:
            X-Request-ID:
              $ref: '#/components/headers/RequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProviderProfile'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/ProviderProfileNotFound'
        '405':
          $ref: '#/components/responses/MethodNotAllowed'
        '406':
          $ref: '#/components/responses/NotAcceptable'
        '500':
          $ref: '#/components/responses/InternalError'
    delete:
      operationId: revokeProviderProfile
      summary: Revoke one caller-owned provider profile
      description: >-
        Idempotent. An active profile is revoked and its retained credential
        material discarded; a profile that is already revoked returns the
        same 204. An unknown or foreign identifier is 404.
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/RequestId'
        - $ref: '#/components/parameters/ProfileId'
      responses:
        '204':
          description: The profile is revoked
          headers:
            X-Request-ID:
              $ref: '#/components/headers/RequestId'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/ProviderProfileNotFound'
        '405':
          $ref: '#/components/responses/MethodNotAllowed'
        '406':
          $ref: '#/components/responses/NotAcceptable'
        '409':
          description: >-
            Concurrent rotations kept superseding the version being revoked;
            the profile stays active and the request may be repeated.
          headers:
            X-Request-ID:
              $ref: '#/components/headers/RequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ProvisionerUnavailable'
  /api/ai-code-review/v1/provider-profiles/{profileId}/rotate:
    post:
      operationId: rotateProviderProfile
      summary: Replace a profile's credential and advance its version
      description: >-
        Accepts the same body as registration. The next version becomes
        active atomically. Removal of the former managed destination is
        attempted once; failure is logged for manual operator cleanup and
        does not undo the rotation. No background retry is scheduled for
        that cleanup. A revoked profile cannot be rotated.
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/RequestId'
        - $ref: '#/components/parameters/ProfileId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProviderProfileRotation'
      responses:
        '200':
          description: The rotated profile
          headers:
            X-Request-ID:
              $ref: '#/components/headers/RequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProviderProfile'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/ProviderProfileNotFound'
        '405':
          $ref: '#/components/responses/MethodNotAllowed'
        '406':
          $ref: '#/components/responses/NotAcceptable'
        '409':
          description: >-
            The profile is revoked, a concurrent change won, or the supplied
            rotation_id and expected_version no longer match their
            reservation (error code
            provider_profile_registration_conflict). A caller at its
            retained-operation maximum receives
            provider_profile_registration_limit. The sealed envelope was not
            consumed.
          headers:
            X-Request-ID:
              $ref: '#/components/headers/RequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '413':
          $ref: '#/components/responses/ContentTooLarge'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '422':
          $ref: '#/components/responses/UnprocessableContent'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ProvisionerUnavailable'
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: '<client-id>.<secret>'
      description: >-
        Deployment-owned client-to-server service token. This is not an APISIX
        or LLM provider credential. The lowercase client ID starts with an
        ASCII letter and then contains only letters, digits, underscores, or
        hyphens.
  parameters:
    RequestId:
      name: X-Request-ID
      in: header
      required: false
      description: >-
        Optional caller correlation identifier. Values containing 1 to 128
        characters from A-Z, a-z, 0-9, dot, underscore, colon, and hyphen are
        retained. Other values are replaced with a server-generated identifier.
      schema:
        type: string
      x-retained-value-schema:
        type: string
        minLength: 1
        maxLength: 128
        pattern: '^[A-Za-z0-9._:-]+$'
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: Opaque key for safely retrying one review submission
      schema:
        type: string
        minLength: 16
        maxLength: 128
        pattern: '^[A-Za-z0-9._:-]+$'
    Prefer:
      name: Prefer
      in: header
      required: false
      description: Set to respond-async to request asynchronous processing
      schema:
        type: string
        enum:
          - respond-async
    ProfileId:
      name: profileId
      in: path
      required: true
      schema:
        type: string
        format: uuid
  headers:
    AuthenticationChallenge:
      description: Service-token authentication challenge
      schema:
        type: string
        const: 'Bearer realm="AI Code Review Server"'
    RequestId:
      description: Server-accepted or server-generated request identifier
      schema:
        type: string
    RetryAfter:
      description: Seconds before the client should retry request admission
      schema:
        type: integer
        minimum: 1
  schemas:
    HealthStatus:
      type: object
      additionalProperties: false
      required:
        - status
      properties:
        status:
          type: string
          enum:
            - ok
            - ready
            - not_ready
    ReviewRequest:
      type: object
      additionalProperties: false
      required:
        - repository_url
        - branch
        - base_commit_id
        - commit_id
        - provider_profile_id
      properties:
        repository_url:
          type: string
          format: uri
          maxLength: 2048
          pattern: '^[Hh][Tt][Tt][Pp][Ss]://(?:\[[0-9A-Fa-f:.]+\]|\[[Vv][0-9A-Fa-f]+\.[A-Za-z0-9._~!$&''()*+,;=:-]+\]|(?:[A-Za-z0-9._~!$&''()*+,;=-]|%[0-9A-Fa-f]{2})+)(?::(?:[0-9]{1,4}|[0-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]))?(?:/(?:[A-Za-z0-9._~!$&''()*+,;=:@-]|%[0-9A-Fa-f]{2})*)*(?:\?(?:[A-Za-z0-9._~!$&''()*+,;=:@/?-]|%[0-9A-Fa-f]{2})*)?(?:#(?:[A-Za-z0-9._~!$&''()*+,;=:@/?-]|%[0-9A-Fa-f]{2})*)?(?![\s\S])'
          description: HTTPS URL with an optional port from 0 through 65535
        branch:
          type: string
          minLength: 1
          maxLength: 255
          pattern: '^(?!HEAD(?![\s\S]))(?![-/])(?!\.)(?![\s\S]*/\.)(?![^/]*\.lock(?:/|(?![\s\S])))(?![\s\S]*/[^/]*\.lock(?:/|(?![\s\S])))(?![\s\S]*(?://|\.\.|@\{|[\x00-\x20\x7F~^:?*\[\\]))(?![\s\S]*[/.](?![\s\S]))[\s\S]+$'
        base_commit_id:
          $ref: '#/components/schemas/CanonicalGitObjectId'
        commit_id:
          $ref: '#/components/schemas/CanonicalGitObjectId'
        output_language:
          type: string
          minLength: 2
          maxLength: 35
          pattern: '^[A-Za-z]{2,8}(-[A-Za-z0-9]{1,8})*(?![\s\S])'
          description: Uses the deployment-configured default when omitted.
        review_policy:
          $ref: '#/components/schemas/ReviewPolicy'
        environments:
          description: >-
            Caller-provided target environments rendered verbatim on one line
            in every chunk. Reject C0/C1 controls, TAB, CR, LF, NEL, U+2028,
            U+2029, bidi controls, Unicode tags, and consecutive variation
            selectors. No policy permission is required for environments alone.
          type: array
          maxItems: 20
          uniqueItems: true
          items:
            type: string
            minLength: 1
            maxLength: 120
        provider_profile_id:
          $ref: '#/components/schemas/ProviderProfileId'
    ReviewPolicy:
      type: object
      additionalProperties: false
      required:
        - format
        - content
      description: >-
        Optional caller criteria, never server instructions. Explicit null is
        invalid. Structural and fixed validation precede owner/key digest replay;
        new policy submissions then require submit ai code review policies
        permission (403 access_denied), followed by the operational
        byte cap (422). Accepted identical replays survive permission revocation
        and operational-cap reduction; changed criteria return 409.
      properties:
        format:
          type: string
          enum:
            - text/markdown
            - text/plain
        content:
          type: string
          minLength: 1
          maxLength: 98307
          x-canonical-maximum-utf8-bytes: 49152
          x-default-submission-maximum-utf8-bytes: 32768
          description: >-
            UTF-8 text. Convert CRLF and CR to LF and remove exactly one leading
            BOM before validation, hashing and storage; otherwise preserve bytes.
            Canonical content must be nonempty and at most 49152 UTF-8 bytes;
            new submissions default to a configurable 32768-byte cap. The larger
            wire maxLength accommodates CRLF and BOM before canonicalization;
            the independent HTTP JSON document ceiling is 1048576 bytes.
            Reject C0/C1 controls except TAB/LF, bidi controls, Unicode tags,
            and consecutive variation selectors. Include the full canonical
            document in every chunk. Source criteria from trusted base/config
            independent of the reviewed head; the server cannot prove provenance.
    ReviewPolicyReceipt:
      type: object
      additionalProperties: false
      required:
        - format
        - sha256
        - bytes
      description: >-
        Present if and only if the submission contained review_policy, including
        accepted replay after terminal input purge. Confirms canonical receipt,
        not model compliance. Absent for no-policy and environments-only requests.
      properties:
        format:
          type: string
          enum:
            - text/markdown
            - text/plain
        sha256:
          type: string
          pattern: '^[0-9a-f]{64}$'
        bytes:
          type: integer
          minimum: 1
          maximum: 49152
    ProviderProfileId:
      type: string
      format: uuid
      minLength: 36
      maxLength: 36
      pattern: '^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}(?![\s\S])'
      description: >-
        Identifier of a provider profile registered by the calling client.
        For a new submission, an unknown, revoked, or foreign identifier is
        rejected with one identical validation error before repository checkout,
        queue allocation, or LLM request. An identical accepted owner/key replay
        returns its original review even after profile rotation or revocation;
        changed canonical input under that key conflicts. The accepted version is
        pinned to the review: a profile rotated or revoked before the worker
        runs fails that review with provider_profile_unavailable instead of
        executing with inputs the caller never saw.
    ProviderProfileRegistration:
      type: object
      additionalProperties: false
      description: >-
        The create request body. It alone accepts registration_id; the rotate
        endpoint rejects that member as unknown and accepts rotation_id with
        expected_version instead.
      required:
        - provider
        - model
        - execution_route
        - sealed_credential
      properties:
        provider:
          type: string
          enum:
            - openai
            - azure_openai
            - anthropic
          description: >-
            Server allow-listed provider adapter. Only openai has a reviewed
            adapter today; the others are refused at registration.
        model:
          type: string
          minLength: 1
          maxLength: 128
          description: Provider model identifier, validated by the adapter.
        execution_route:
          type: string
          enum:
            - apisix_managed
            - direct_server
        sealed_credential:
          type: string
          writeOnly: true
          minLength: 1
          maxLength: 8192
          pattern: '^[A-Za-z0-9_-]+(?![\s\S])'
          description: >-
            One-time base64url envelope: a JSON object with members v (1),
            kid (registration key identifier), and sealed (a libsodium
            sealed box, base64url) whose plaintext is a JSON object with
            members client_id (the caller's service-token client ID),
            issued_at (Unix seconds), nonce (base64url, 16-64 bytes), and
            credential (the provider API key). It is never returned, logged,
            or retained; a replayed nonce is refused.
        registration_id:
          type: string
          pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}(?![\s\S])'
          description: >-
            Optional, non-secret lowercase canonical UUID generated once by
            the client for one logical create. Supplying it makes a retry of
            that create converge on one profile identifier, one stored
            credential, and one APISIX route; the completed replay returns
            the existing profile with 201 without opening the newly supplied
            envelope. Reusing it with a different provider, model, or route
            returns provider_profile_registration_conflict, and a replay
            after revocation returns
            provider_profile_registration_revoked. A create without this
            member remains accepted and is treated as a new logical create.
    ProviderProfileRotation:
      type: object
      additionalProperties: false
      description: >-
        The rotate request body. It carries the same four registration
        members and, unlike create, accepts rotation_id and expected_version.
        The create member registration_id is rejected here as unknown before
        the sealed envelope is opened.
      required:
        - provider
        - model
        - execution_route
        - sealed_credential
      properties:
        provider:
          $ref: '#/components/schemas/ProviderProfileRegistration/properties/provider'
        model:
          $ref: '#/components/schemas/ProviderProfileRegistration/properties/model'
        execution_route:
          $ref: '#/components/schemas/ProviderProfileRegistration/properties/execution_route'
        sealed_credential:
          $ref: '#/components/schemas/ProviderProfileRegistration/properties/sealed_credential'
        rotation_id:
          type: string
          pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}(?![\s\S])'
          description: >-
            Optional, non-secret lowercase canonical UUID identifying one
            logical rotation. It must be sent together with
            expected_version; one member alone is rejected with 422. A
            retry that repeats both reuses the reserved target version, so a
            managed rotation cannot materialize two routes.
        expected_version:
          type: integer
          minimum: 1
          description: >-
            The active profile version the client rotated from. It must be
            sent together with rotation_id. A value that does not match the
            active version returns
            provider_profile_registration_conflict before the sealed
            envelope is opened.
    ProviderProfile:
      type: object
      additionalProperties: false
      required:
        - id
        - provider
        - model
        - execution_route
        - status
        - version
        - created_at
        - updated_at
      properties:
        id:
          $ref: '#/components/schemas/ProviderProfileId'
        provider:
          type: string
        model:
          type: string
        execution_route:
          type: string
          enum:
            - apisix_managed
            - direct_server
        status:
          type: string
          enum:
            - active
            - revoked
        version:
          type: integer
          minimum: 1
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    ProviderProfileCollection:
      type: object
      additionalProperties: false
      required:
        - profiles
      properties:
        profiles:
          type: array
          items:
            $ref: '#/components/schemas/ProviderProfile'
    ProviderProfileCapabilities:
      type: object
      additionalProperties: false
      required:
        - registration
        - providers
      properties:
        registration:
          $ref: '#/components/schemas/ProviderProfileRegistrationCapabilities'
        providers:
          type: array
          items:
            $ref: '#/components/schemas/ProviderProfileCapability'
    ProviderProfileRegistrationCapabilities:
      type: object
      additionalProperties: false
      required:
        - envelope_version
        - algorithm
        - key_id
        - public_key
        - maximum_envelope_age_seconds
      properties:
        envelope_version:
          type: integer
          const: 1
        algorithm:
          type: string
          const: sodium_crypto_box_seal
        key_id:
          type: string
          minLength: 1
          maxLength: 64
          pattern: '^[A-Za-z0-9][A-Za-z0-9._-]{0,63}(?![\s\S])'
        public_key:
          type: string
          minLength: 43
          maxLength: 43
          pattern: '^[A-Za-z0-9_-]{43}(?![\s\S])'
          description: Exactly 32 raw public-key bytes in unpadded base64url.
        maximum_envelope_age_seconds:
          type: integer
          minimum: 1
    ProviderProfileCapability:
      type: object
      additionalProperties: false
      required:
        - id
        - execution_routes
      properties:
        id:
          type: string
          minLength: 1
          maxLength: 64
          pattern: '^[a-z][a-z0-9_]*(?![\s\S])'
        execution_routes:
          type: array
          minItems: 1
          uniqueItems: true
          items:
            type: string
            enum:
              - apisix_managed
              - direct_server
    CanonicalGitObjectId:
      type: string
      description: Strict full SHA-1 or SHA-256 Git object identifier
      pattern: '^(?:[0-9a-fA-F]{40}|[0-9a-fA-F]{64})(?![\s\S])'
    ReviewJob:
      type: object
      additionalProperties: false
      required:
        - review_id
        - result_url
        - status
        - status_url
        - submitted_at
      properties:
        review_policy:
          $ref: '#/components/schemas/ReviewPolicyReceipt'
        review_id:
          type: string
          format: uuid
        status:
          type: string
          description: Fixed v1 asynchronous-submission acceptance state.
          const: queued
        status_url:
          type: string
          format: uri-reference
        result_url:
          type: string
          format: uri-reference
        submitted_at:
          type: string
          format: date-time
        expires_at:
          type: string
          format: date-time
    ReviewResult:
      type: object
      additionalProperties: false
      required:
        - review_id
        - status
        - findings
        - summary
        - rendered_result
        - completed_at
      properties:
        review_id:
          type: string
          format: uuid
        status:
          type: string
          const: completed
        findings:
          type: array
          items:
            $ref: '#/components/schemas/Finding'
        summary:
          $ref: '#/components/schemas/ReviewSummary'
        rendered_result:
          type: string
          description: Bounded plain-text report with Summary and Detail and dynamically sized backtick code fences; no ANSI escapes or HTML rendering
        completed_at:
          type: string
          format: date-time
    ReviewStatus:
      type: object
      additionalProperties: false
      required:
        - attempt
        - created_at
        - error_code
        - expires_at
        - finished_at
        - review_id
        - started_at
        - status
      properties:
        attempt:
          type: integer
          minimum: 0
        created_at:
          type: string
          format: date-time
        error_code:
          type:
            - string
            - 'null'
        expires_at:
          type:
            - string
            - 'null'
          format: date-time
        finished_at:
          type:
            - string
            - 'null'
          format: date-time
        review_id:
          type: string
          format: uuid
        started_at:
          type:
            - string
            - 'null'
          format: date-time
        status:
          type: string
          enum:
            - cancelled
            - completed
            - expired
            - failed
            - queued
            - running
    Finding:
      type: object
      additionalProperties: false
      description: Actionable LLM feedback only; analyzer records remain review context and are not returned as findings
      required:
        - finding_id
        - severity
        - category
        - message
        - path
        - line
        - suggestion
        - suggestion_format
      properties:
        finding_id:
          type: string
          minLength: 1
          maxLength: 128
        severity:
          type: string
          enum:
            - critical
            - high
            - medium
            - low
            - info
        category:
          type: string
          minLength: 1
          maxLength: 80
        message:
          type: string
          minLength: 1
          maxLength: 4000
        path:
          type: string
          minLength: 1
          maxLength: 1024
        line:
          type: integer
          minimum: 1
        end_line:
          type: [integer, "null"]
          minimum: 1
        function:
          type: [string, "null"]
          minLength: 1
          maxLength: 256
          description: Named function or method supported by supplied context; null when unavailable
        suggestion:
          type: string
          minLength: 1
          maxLength: 8000
          description: Nonblank concrete corrected code or unified diff; never applied automatically
        suggestion_format:
          type: string
          enum: [code, unified_diff]
    ReviewSummary:
      type: object
      additionalProperties: false
      required:
        - files_reviewed
        - finding_count
      properties:
        files_reviewed:
          type: integer
          minimum: 0
        finding_count:
          type: integer
          minimum: 0
        truncated:
          type: boolean
          default: false
          description: True when review material, findings, or rendered report bytes were omitted
    ErrorResponse:
      type: object
      additionalProperties: false
      required:
        - error
      properties:
        error:
          type: object
          additionalProperties: false
          required:
            - code
            - message
            - request_id
          properties:
            code:
              type: string
              pattern: '^[a-z][a-z0-9_]*$'
            message:
              type: string
              description: >-
                Localized human-readable message; clients must use code for
                program logic. An oversized translation falls back to the
                bounded English source message.
              minLength: 1
              maxLength: 1000
            request_id:
              type: string
            details:
              type: array
              maxItems: 100
              items:
                type: object
                additionalProperties: false
                required:
                  - path
                  - code
                  - message
                properties:
                  path:
                    type: string
                    description: JSON Pointer to the invalid request value
                  code:
                    type: string
                  message:
                    type: string
                    description: Localized human-readable validation message
  responses:
    BadRequest:
      description: Malformed framing, malformed JSON, or duplicate properties within structural limits
      headers:
        X-Request-ID:
          $ref: '#/components/headers/RequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Unauthorized:
      description: Missing or invalid service authentication
      headers:
        WWW-Authenticate:
          $ref: '#/components/headers/AuthenticationChallenge'
        X-Request-ID:
          $ref: '#/components/headers/RequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Forbidden:
      description: Authenticated caller is not allowed to perform this review
      headers:
        X-Request-ID:
          $ref: '#/components/headers/RequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    MethodNotAllowed:
      description: The request method is not supported by this endpoint
      headers:
        Allow:
          description: Methods supported by this endpoint
          schema:
            type: string
        X-Request-ID:
          $ref: '#/components/headers/RequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    NotAcceptable:
      description: The requested response representation is not available
      headers:
        X-Request-ID:
          $ref: '#/components/headers/RequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Conflict:
      description: Idempotency conflict or incompatible repository state
      headers:
        X-Request-ID:
          $ref: '#/components/headers/RequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    ContentTooLarge:
      description: Request, repository, or diff exceeds a limit evaluated before duplicate detection
      headers:
        X-Request-ID:
          $ref: '#/components/headers/RequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    UnsupportedMediaType:
      description: The request content type or content encoding is not supported
      headers:
        X-Request-ID:
          $ref: '#/components/headers/RequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    UnprocessableContent:
      description: Depth, request fields, or Git references fail validation
      headers:
        X-Request-ID:
          $ref: '#/components/headers/RequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    TooManyRequests:
      description: Per-client rate or concurrency limit, or asynchronous review backlog capacity, was exceeded
      headers:
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
        X-Request-ID:
          $ref: '#/components/headers/RequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    InternalError:
      description: Unexpected internal failure
      headers:
        X-Request-ID:
          $ref: '#/components/headers/RequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    BadGateway:
      description: >-
        APISIX or an LLM provider returned an invalid response, or an enabled
        private language analyzer returned malformed output or failed its tool.
        No analyzer path, source, tool output, or private sidecar outcome is returned.
      headers:
        X-Request-ID:
          $ref: '#/components/headers/RequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Unavailable:
      description: >-
        Review service or a required dependency is unavailable, including an
        enabled language analyzer whose compatible sidecar is not ready.
        Disabled review submission uses the canonical service_unavailable code
        after authentication and route permission, without accepting work.
      headers:
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
        X-Request-ID:
          $ref: '#/components/headers/RequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    ProviderProfileNotFound:
      description: The profile does not exist or is not owned by the caller
      headers:
        X-Request-ID:
          $ref: '#/components/headers/RequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    ProvisionerUnavailable:
      description: >-
        The APISIX credential provisioner is unavailable (error code
        provider_provisioner_unavailable). New managed creates require a
        durable, versioned idempotency client; an interface-only provisioner
        is refused before the credential envelope is opened or a capacity
        slot is reserved. A failure after a remote attempt is ambiguous:
        an identified create or rotation has reserved its durable operation,
        and a managed rotation may already have advanced the profile and
        queued its superseded route for removal. Reconcile by replaying the
        same registration_id, or the same rotation_id and expected_version,
        rather than starting a new logical operation. An attempted request
        without an identifier is reconciled by the server's own maintenance.
      headers:
        X-Request-ID:
          $ref: '#/components/headers/RequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    ProviderProfileRegistrationUnavailable:
      description: The registration key material is unavailable or unusable
      headers:
        X-Request-ID:
          $ref: '#/components/headers/RequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    GatewayTimeout:
      description: >-
        Review, private language analyzer, or backend LLM processing exceeded
        its deadline.
      headers:
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
        X-Request-ID:
          $ref: '#/components/headers/RequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
