openapi: 3.1.0
info:
  title: Multipolarity Public API
  version: 0.1.0
  summary: Target public API contract for forecast, foresight, usage, and OpenAI-style transport.
  description: >
    Initial OpenAPI skeleton for the productized Multipolarity /v1 API. The
    native forecast API is canonical. OpenAI-style endpoints are compatibility
    adapters over the same forecast runtime.
servers:
  - url: https://api.multipolarity.ai
    description: Production target
  - url: http://localhost:3001
    description: Local App facade during development
security:
  - ApiKeyBearer: []
tags:
  - name: Health
  - name: Forecasts
  - name: Usage
  - name: OpenAI Compatibility
paths:
  /v1/health:
    get:
      tags: [Health]
      summary: Public API liveness
      security: []
      responses:
        "200":
          description: API process is alive.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HealthResponse"
  /v1/status:
    get:
      tags: [Health]
      summary: Authenticated API readiness and route availability
      responses:
        "200":
          description: Current readiness summary.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StatusResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
  /v1/models:
    get:
      tags: [OpenAI Compatibility]
      summary: List public model and capability aliases
      description: >
        Returns OpenAI-style model objects for Multipolarity capability aliases.
        Aliases route to native forecast capabilities rather than raw base models.
      responses:
        "200":
          description: Model alias list.
          content:
            application/json:
              schema:
                type: object
                required: [object, data]
                properties:
                  object:
                    const: list
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Model"
        "401":
          $ref: "#/components/responses/Unauthorized"
  /v1/forecasts:
    post:
      tags: [Forecasts]
      summary: Submit a native forecast or foresight request
      description: >
        Canonical forecast endpoint. Low-effort routes may complete synchronously when
        already available within budget. Otherwise the endpoint returns 202 with
        a durable run id and polling/events URLs.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ForecastCreateRequest"
            examples:
              brentLow:
                summary: Low-effort graph closed forecast
                value:
                  question: Will Brent exceed USD 95 before 2026-09-30?
                  theme_id: energy
                  horizon:
                    end: "2026-09-30"
                  route:
                    method: auto
                    effort: low
                    allow_new_variables: true
                  research: off
                  persist: false
                  stream: true
                  max_credits: 50
                  metadata:
                    client_run_id: brent-risk-bot-2026-07-06-001
      responses:
        "200":
          description: Forecast completed synchronously.
          headers:
            X-MP-Request-ID:
              $ref: "#/components/headers/X-MP-Request-ID"
            X-MP-Run-ID:
              $ref: "#/components/headers/X-MP-Run-ID"
            X-MP-Credits-Used:
              $ref: "#/components/headers/X-MP-Credits-Used"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Forecast"
        "202":
          description: Forecast accepted for async execution.
          headers:
            X-MP-Request-ID:
              $ref: "#/components/headers/X-MP-Request-ID"
            X-MP-Run-ID:
              $ref: "#/components/headers/X-MP-Run-ID"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ForecastAccepted"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/InsufficientCredits"
        "409":
          $ref: "#/components/responses/IdempotencyConflict"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"
  /v1/forecasts/{forecast_id}:
    get:
      tags: [Forecasts]
      summary: Retrieve forecast status and answer surface
      parameters:
        - $ref: "#/components/parameters/ForecastId"
      responses:
        "200":
          description: Forecast status or terminal answer.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Forecast"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
  /v1/forecasts/{forecast_id}/events:
    get:
      tags: [Forecasts]
      summary: Stream forecast progress events
      description: >
        Server-sent events stream backed by Redis/Mongo state. Clients should
        reconnect with Last-Event-ID where supported.
      parameters:
        - $ref: "#/components/parameters/ForecastId"
      responses:
        "200":
          description: SSE stream.
          content:
            text/event-stream:
              schema:
                type: string
                examples:
                  - "event: progress\ndata: {\"status\":\"running\",\"progress\":{\"state\":\"running\"}}\n\n"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
  /v1/forecasts/{forecast_id}/cancel:
    post:
      tags: [Forecasts]
      summary: Best-effort cancel for queued or running forecast
      parameters:
        - $ref: "#/components/parameters/ForecastId"
      responses:
        "200":
          description: Cancellation state.
          content:
            application/json:
              schema:
                type: object
                required: [id, canceled]
                properties:
                  id:
                    type: string
                  canceled:
                    type: boolean
                  status:
                    type: string
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
  /v1/inference:
    post:
      tags: [Inference]
      summary: Submit a generic inference run (capability-parameterized)
      description: >
        The generic tenant-run producer. `capability` (default `foresight`) selects the runtime
        capability. `foresight` takes the same inputs as `/v1/forecasts`. `risk_exposure` is registered
        but not yet available on the public API (returns 400). `risk_indicator_inference` is a degraded
        route with no durable executor (returns 501). Reserves credits before enqueue; idempotent on
        `Idempotency-Key`. Returns 202 with a run id (200 on an idempotent replay).
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [question]
              properties:
                capability:
                  type: string
                  enum: [foresight, risk_exposure]
                  default: foresight
                  description: Runtime capability. Only `foresight` is currently enqueue-able.
                question:
                  type: string
                  minLength: 3
                  description: The forecast question (foresight capability).
                mode:
                  type: string
                  enum: [foresight, prediction, analytical]
                horizon:
                  type: object
                  properties:
                    value: { type: number }
                    unit: { type: string }
            examples:
              foresight:
                summary: Foresight run
                value:
                  capability: foresight
                  question: Will Brent exceed USD 95 before 2026-09-30?
                  horizon: { value: 3, unit: months }
      responses:
        "200":
          description: Idempotent replay of an existing run.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/InferenceAccepted" }
        "202":
          description: Inference run accepted for async execution.
          headers:
            X-MP-Request-ID:
              $ref: "#/components/headers/X-MP-Request-ID"
            X-MP-Run-ID:
              $ref: "#/components/headers/X-MP-Run-ID"
          content:
            application/json:
              schema: { $ref: "#/components/schemas/InferenceAccepted" }
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/InsufficientCredits"
        "409":
          $ref: "#/components/responses/IdempotencyConflict"
        "429":
          $ref: "#/components/responses/RateLimited"
        "501":
          description: The requested capability has no durable public executor.
        "503":
          $ref: "#/components/responses/ServiceUnavailable"
  /v1/inference/{run_id}:
    get:
      tags: [Inference]
      summary: Retrieve inference run status
      parameters:
        - name: run_id
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Run status.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/InferenceRun" }
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
  /v1/inference/{run_id}/cancel:
    post:
      tags: [Inference]
      summary: Best-effort cancel for a queued or running inference run
      parameters:
        - name: run_id
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Cancellation state.
          content:
            application/json:
              schema:
                type: object
                properties:
                  runId: { type: string }
                  status: { type: string }
                  refund: { type: string, enum: [full, partial] }
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: The run is already terminal.
  /v1/inference/{run_id}/stream:
    get:
      tags: [Inference]
      summary: Stream inference run progress (SSE)
      description: >
        Server-sent events backed by Redis/Mongo state. Reconnect with `Last-Event-ID` to replay
        milestones after the last received index.
      parameters:
        - name: run_id
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: SSE stream.
          content:
            text/event-stream:
              schema: { type: string }
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
  /v1/usage:
    get:
      tags: [Usage]
      summary: API usage, credits, and rate limits
      parameters:
        - name: window_days
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 366
            default: 30
      responses:
        "200":
          description: Usage summary.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UsageSummary"
        "401":
          $ref: "#/components/responses/Unauthorized"
  /v1/responses:
    post:
      tags: [OpenAI Compatibility]
      summary: OpenAI-style Responses adapter
      description: >
        Compatibility adapter over /v1/forecasts. The native forecast object
        remains canonical; structured forecast data is returned under mp.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/OpenAiResponseRequest"
      responses:
        "200":
          description: Completed OpenAI-style response.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OpenAiResponse"
        "202":
          description: Accepted async forecast response.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OpenAiResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
  /v1/chat/completions:
    post:
      tags: [OpenAI Compatibility]
      summary: OpenAI-style Chat Completions adapter
      description: >
        Compatibility shim for older clients. The final user message maps to
        the native forecast question and the structured forecast is returned
        under mp.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ChatCompletionRequest"
      responses:
        "200":
          description: Chat-style forecast completion.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ChatCompletionResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
  /v1/responses/{response_id}:
    get:
      tags: [OpenAI Compatibility]
      summary: Retrieve a Response (async forecast result)
      parameters:
        - { name: response_id, in: path, required: true, schema: { type: string } }
      responses:
        "200": { description: The Response object; output_text is populated once completed. }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
  /v1/forecasts/{forecast_id}/stream:
    get:
      tags: [Forecasts]
      summary: Reconnectable SSE stream of a forecast's milestones + status
      parameters:
        - $ref: "#/components/parameters/ForecastId"
      responses:
        "200": { description: "text/event-stream of milestone / status / done frames." }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
  /v1/counterfactual:
    post:
      tags: [Inference]
      summary: Counterfactual belief delta for a scenario variable (premium)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [questionId, override]
              properties:
                questionId: { type: string }
                override:
                  type: object
                  required: [kind, id, value]
                  properties:
                    kind: { type: string, enum: [claim, variable, market] }
                    id: { type: string }
                    value: {}
      responses:
        "200": { description: "{ delta, baseBelief, diagnostics }." }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { description: Insufficient tier. }
  /v1/cortex/query:
    post:
      tags: [Cortex]
      summary: Query the tenant's Cortex knowledge graph around a seed node
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [seed]
              properties:
                seed:
                  type: object
                  required: [kind, id]
                  properties: { kind: { type: string }, id: { type: string } }
                mode: { type: string, enum: [subgraph, neighbors], default: subgraph }
                opts: { type: object }
      responses:
        "200": { description: The KG subgraph or neighbors result. }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
  /v1/risk/exposures:
    get:
      tags: [Risk]
      summary: List the tenant's exposure assets (member_portfolio excluded)
      responses:
        "200": { description: "{ exposures: [{ asset, exposedObject }] }." }
        "401": { $ref: "#/components/responses/Unauthorized" }
  /v1/risk/exposures/{exposure_id}:
    get:
      tags: [Risk]
      summary: Retrieve a single exposure asset view
      parameters:
        - { name: exposure_id, in: path, required: true, schema: { type: string } }
      responses:
        "200": { description: The ExposureAssetView. }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
  /v1/risk/exposures/{exposure_id}/graph:
    get:
      tags: [Risk]
      summary: The SerializedGraph centered on an exposure asset
      parameters:
        - { name: exposure_id, in: path, required: true, schema: { type: string } }
      responses:
        "200": { description: The exposure asset graph. }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
  /v1/webhooks:
    get:
      tags: [Webhooks]
      summary: List the tenant's webhook subscriptions (secret never returned)
      responses:
        "200": { description: "{ webhooks: [...] }." }
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      tags: [Webhooks]
      summary: Subscribe a URL to events (returns the signing secret ONCE)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url: { type: string, format: uri }
                events: { type: array, items: { type: string, enum: [forecast.completed, forecast.failed] } }
      responses:
        "201": { description: "The subscription incl. its one-time `secret`." }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
  /v1/webhooks/{webhook_id}:
    delete:
      tags: [Webhooks]
      summary: Delete a webhook subscription
      parameters:
        - { name: webhook_id, in: path, required: true, schema: { type: string } }
      responses:
        "200": { description: Deleted. }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
  /v1/explain/{prediction_id}:
    get:
      tags: [Inference]
      summary: Explain a prediction (belief + trace)
      parameters:
        - { name: prediction_id, in: path, required: true, schema: { type: string } }
      responses:
        "200": { description: The explanation. }
        "401": { $ref: "#/components/responses/Unauthorized" }
  /v1/explain/{prediction_id}/chain:
    get:
      tags: [Inference]
      summary: The trace chain for a prediction
      parameters:
        - { name: prediction_id, in: path, required: true, schema: { type: string } }
      responses:
        "200": { description: The trace chain. }
        "401": { $ref: "#/components/responses/Unauthorized" }
  /v1/epistemic/{theme_id}/latest:
    get:
      tags: [Inference]
      summary: Latest epistemic snapshot for a theme
      parameters:
        - { name: theme_id, in: path, required: true, schema: { type: string } }
      responses:
        "200": { description: The latest theme beliefs + recent changes. }
        "401": { $ref: "#/components/responses/Unauthorized" }
  /v1/calibration:
    get:
      tags: [Inference]
      summary: Calibration / track-record metrics
      parameters:
        - { name: themeId, in: query, required: true, schema: { type: string } }
        - { name: windowDays, in: query, schema: { type: integer, default: 30 } }
      responses:
        "200": { description: Calibration metrics. }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
  /v1/arbitrage:
    get:
      tags: [Inference]
      summary: Arbitrage signals (experimental)
      responses:
        "200": { description: Arbitrage signals. }
        "401": { $ref: "#/components/responses/Unauthorized" }
  /v1/arbitrage/stream:
    get:
      tags: [Inference]
      summary: SSE stream of arbitrage signals (experimental)
      responses:
        "200": { description: "text/event-stream of arbitrage signals." }
        "401": { $ref: "#/components/responses/Unauthorized" }
  /v1/openapi.yaml:
    get:
      tags: [Health]
      summary: This OpenAPI document (public, unauthenticated)
      security: []
      responses:
        "200":
          description: The spec, as YAML.
          content:
            application/yaml:
              schema: { type: string }
components:
  securitySchemes:
    ApiKeyBearer:
      type: http
      scheme: bearer
      bearerFormat: mp_live API key
      description: "Authorization: Bearer mp_live_..."
  parameters:
    ForecastId:
      name: forecast_id
      in: path
      required: true
      schema:
        type: string
        pattern: "^fcst_[A-Za-z0-9_:-]+$"
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      schema:
        type: string
        minLength: 8
        maxLength: 200
  headers:
    X-MP-Request-ID:
      schema:
        type: string
      description: Multipolarity request id.
    X-MP-Run-ID:
      schema:
        type: string
      description: Forecast or inference run id.
    X-MP-Credits-Used:
      schema:
        type: number
      description: Finalized credits charged for the request.
    X-MP-Credits-Reserved:
      schema:
        type: number
      description: Credits reserved before execution.
    X-MP-Route:
      schema:
        type: string
      description: Selected route and effort.
    X-MP-Result-Status:
      schema:
        type: string
      description: Public forecast result status.
  responses:
    BadRequest:
      description: Invalid request.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Unauthorized:
      description: Missing or invalid API key.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    InsufficientCredits:
      description: Request requires more credits than available or allowed by max_credits.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    IdempotencyConflict:
      description: Idempotency key reused with a different request body.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    RateLimited:
      description: Rate, concurrency, queue, or quota limit exceeded.
      headers:
        Retry-After:
          schema:
            type: integer
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    ServiceUnavailable:
      description: Dependency unavailable or queue admission closed.
      headers:
        Retry-After:
          schema:
            type: integer
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    NotFound:
      description: Resource not found or not visible to this API key.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
  schemas:
    HealthResponse:
      type: object
      required: [status, timestamp]
      properties:
        status:
          enum: [ok, degraded]
        timestamp:
          type: string
          format: date-time
    StatusResponse:
      type: object
      required: [status, routes, dependencies]
      properties:
        status:
          enum: [ready, degraded, unavailable]
        routes:
          type: object
          additionalProperties:
            type: object
            properties:
              available:
                type: boolean
              reason:
                type: string
        dependencies:
          type: object
          additionalProperties:
            enum: [ok, degraded, unavailable]
        queue:
          type: object
          properties:
            accepting:
              type: boolean
            retry_after_seconds:
              type: integer
    Model:
      type: object
      required: [id, object, owned_by]
      properties:
        id:
          type: string
          examples: [mp-foresight-low, mp-foresight-high, mp-foresight-research, mp-risk-exposure]
        object:
          const: model
        owned_by:
          const: multipolarity
        capability:
          enum: [foresight, risk_exposure]
        default_effort:
          enum: [low, high]
    ForecastCreateRequest:
      type: object
      required: [question]
      properties:
        question:
          type: string
          minLength: 1
          maxLength: 4000
        theme_id:
          type: string
        horizon:
          type: object
          properties:
            end:
              type: string
              format: date
            label:
              type: string
        route:
          $ref: "#/components/schemas/ForecastRouteRequest"
        research:
          enum: [off, bounded, deep]
          default: off
        persist:
          type: boolean
          default: false
        stream:
          type: boolean
          default: false
        max_credits:
          type: number
          minimum: 0
        allow_degrade:
          type: boolean
          default: false
        metadata:
          type: object
          additionalProperties: true
    ForecastRouteRequest:
      type: object
      properties:
        method:
          enum: [auto, causal_graph, judgemental]
          default: auto
        effort:
          enum: [low, high]
          default: low
        allow_new_variables:
          type: boolean
          default: true
        judge_count:
          type: integer
          minimum: 1
          maximum: 7
    InferenceAccepted:
      type: object
      required: [id, object, capability, status]
      properties:
        id:
          type: string
        object:
          const: inference
        capability:
          type: string
          description: The runtime capability this run executes (e.g. `foresight`).
        status:
          enum: [queued, running]
        replayed:
          type: boolean
          description: True when an idempotent replay returned an existing run.
    InferenceRun:
      type: object
      required: [id, object, capability, status]
      properties:
        id:
          type: string
        object:
          const: inference
        capability:
          type: string
        status:
          enum: [queued, running, completed, failed, cancelled, canceled]
        question:
          type: string
          nullable: true
        mode:
          type: string
          nullable: true
        createdAt:
          type: integer
          nullable: true
        startedAt:
          type: integer
          nullable: true
        completedAt:
          type: integer
          nullable: true
        failedAt:
          type: integer
          nullable: true
        milestones:
          type: array
          items:
            type: object
        terminalSummary:
          nullable: true
    ForecastAccepted:
      type: object
      required: [id, object, status, events_url, poll_url]
      properties:
        id:
          type: string
        object:
          const: forecast
        status:
          enum: [queued, running]
        poll_url:
          type: string
        events_url:
          type: string
        usage:
          $ref: "#/components/schemas/Usage"
    Forecast:
      type: object
      required: [id, object, status, result_status, created]
      properties:
        id:
          type: string
        object:
          const: forecast
        status:
          enum: [queued, running, completed, failed, canceled]
        result_status:
          enum: [running, usable, usable_partial, degraded, dropped, failed]
        created:
          type: integer
        completed:
          type: integer
        route:
          $ref: "#/components/schemas/ForecastRoute"
        output_text:
          type: string
        answer:
          $ref: "#/components/schemas/ForecastAnswer"
        graph:
          $ref: "#/components/schemas/ForecastGraph"
        progress:
          type: object
          additionalProperties: true
        usage:
          $ref: "#/components/schemas/Usage"
        metadata:
          type: object
          additionalProperties: true
    ForecastRoute:
      type: object
      properties:
        method:
          enum: [causal_graph, judgemental]
        effort:
          enum: [low, high]
        query_kind:
          enum: [closed_foresight, open_foresight, scenario_assessment, exposure, what_if, ablation]
        route_source:
          enum: [user_explicit, inferred, defaulted]
        rationale:
          type: string
    ForecastAnswer:
      type: object
      properties:
        root_probabilities:
          type: object
          additionalProperties:
            type: number
        root_posterior:
          type: object
          additionalProperties: true
        key_drivers:
          type: array
          items:
            type: object
            additionalProperties: true
    ForecastGraph:
      type: object
      properties:
        status:
          enum: [committed, dropped, not_applicable, unknown]
        graph_id:
          type: string
        node_count:
          type: integer
        edge_count:
          type: integer
        flags:
          type: array
          items:
            type: string
        soft_flags:
          type: array
          items:
            type: string
    Usage:
      type: object
      properties:
        credits_reserved:
          type: number
        credits_used:
          type: number
        input_tokens:
          type: integer
        output_tokens:
          type: integer
        cost_usd:
          type:
            - number
            - "null"
    UsageSummary:
      type: object
      required: [window_start, window_end, total_credits]
      properties:
        window_start:
          type: string
          format: date-time
        window_end:
          type: string
          format: date-time
        total_calls:
          type: integer
        total_credits:
          type: number
        by_route:
          type: array
          items:
            type: object
            additionalProperties: true
        rate_limits:
          type: object
          additionalProperties: true
    OpenAiResponseRequest:
      type: object
      required: [model, input]
      properties:
        model:
          type: string
          examples: [mp-foresight-low]
        input:
          oneOf:
            - type: string
            - type: array
              items:
                type: object
                additionalProperties: true
        stream:
          type: boolean
          default: false
        metadata:
          type: object
          additionalProperties: true
    OpenAiResponse:
      type: object
      description: >
        TARGET, NOT YET SDK-FAITHFUL. The official OpenAI Responses object returns a
        typed `output` ARRAY (`output[].content[].{type:'output_text', text}`); `output_text`
        is an SDK-only convenience the client derives by walking `output[]`. This schema
        currently exposes only the top-level `output_text`, so a stock `openai` SDK client
        reading `resp.output_text` would get `undefined`. The SDK-faithful `output[]` array
        is added when the compatibility layer is implemented (adversarial review sprint S4.1);
        until then treat this as "OpenAI-style", not drop-in compatible.
      required: [id, object, status, model]
      properties:
        id:
          type: string
        object:
          const: response
        status:
          enum: [queued, in_progress, completed, failed]
        model:
          type: string
        output_text:
          type: string
        usage:
          type: object
          additionalProperties: true
        mp:
          type: object
          additionalProperties: true
    ChatCompletionRequest:
      type: object
      required: [model, messages]
      properties:
        model:
          type: string
        messages:
          type: array
          items:
            type: object
            required: [role, content]
            properties:
              role:
                enum: [system, user, assistant, tool]
              content:
                type: string
        stream:
          type: boolean
          default: false
        metadata:
          type: object
          additionalProperties: true
    ChatCompletionResponse:
      type: object
      required: [id, object, choices]
      properties:
        id:
          type: string
        object:
          const: chat.completion
        choices:
          type: array
          items:
            type: object
            additionalProperties: true
        usage:
          type: object
          additionalProperties: true
        mp:
          type: object
          additionalProperties: true
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
            message:
              type: string
            request_id:
              type: string
            retryable:
              type: boolean
