openapi: 3.1.0
info:
  title: Modelux Management API
  version: 1.0.0
  description: |
    Management API for the Modelux LLM proxy platform. Provides endpoints for
    managing organizations, projects, provider credentials, API keys, routing
    configs, budgets, webhooks, simulations, logs, analytics, and audit trails.

    All success responses wrap data in `{ "data": ... }`. All error responses
    return `{ "error": "message", "code": "error_code" }`. Fields use snake_case.
    Dates are ISO 8601 strings.
  contact:
    name: Modelux
servers:
  - url: /api/manage/v1
    description: Management API base path

security:
  - BearerAuth: []

tags:
  - name: Organizations
    description: Manage your organization
  - name: Members
    description: Manage organization members
  - name: Projects
    description: Manage projects
  - name: Providers
    description: Manage provider credentials (OpenAI, Anthropic, Google, etc.)
  - name: API Keys
    description: Manage proxy API keys
  - name: Routing Configs
    description: Manage routing configurations and versions
  - name: Logs
    description: View request logs, decision traces, and replay requests
  - name: Analytics
    description: Analytics reports and decision summaries
  - name: Budgets
    description: Manage spend budgets, alerts, and events
  - name: Webhooks
    description: Manage webhook endpoints and deliveries
  - name: Simulations
    description: Run routing simulations against historical traffic
  - name: Audit Logs
    description: View audit trail of management actions
  - name: Management Keys
    description: Manage management API keys

paths:
  # ── Organizations ──────────────────────────────────────────────────
  /orgs/{id}:
    patch:
      operationId: updateOrg
      tags: [Organizations]
      summary: Update organization
      parameters:
        - $ref: "#/components/parameters/OrgId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  description: Organization display name
                billing_email:
                  type: string
                  format: email
                  description: Billing contact email
      responses:
        "200":
          description: Updated organization
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Org"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
    delete:
      operationId: deleteOrg
      tags: [Organizations]
      summary: Delete organization
      parameters:
        - $ref: "#/components/parameters/OrgId"
      responses:
        "200":
          description: Organization deleted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DeleteResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  # ── Members ────────────────────────────────────────────────────────
  /orgs/{id}/members:
    get:
      operationId: listMembers
      tags: [Members]
      summary: List organization members
      parameters:
        - $ref: "#/components/parameters/OrgId"
      responses:
        "200":
          description: List of members
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Member"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
    post:
      operationId: inviteMember
      tags: [Members]
      summary: Invite a member to the organization
      parameters:
        - $ref: "#/components/parameters/OrgId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, role]
              properties:
                email:
                  type: string
                  format: email
                  description: Email of the user to invite
                role:
                  type: string
                  enum: [owner, admin, member]
                  description: Role to assign
      responses:
        "201":
          description: Invitation sent
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    description: Invitation details
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /orgs/{id}/members/{userId}:
    patch:
      operationId: updateMemberRole
      tags: [Members]
      summary: Update a member's role
      parameters:
        - $ref: "#/components/parameters/OrgId"
        - $ref: "#/components/parameters/UserId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [role]
              properties:
                role:
                  type: string
                  enum: [owner, admin, member]
      responses:
        "200":
          description: Member updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Member"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
    delete:
      operationId: removeMember
      tags: [Members]
      summary: Remove a member from the organization
      parameters:
        - $ref: "#/components/parameters/OrgId"
        - $ref: "#/components/parameters/UserId"
      responses:
        "200":
          description: Member removed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DeleteResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  # ── Projects ───────────────────────────────────────────────────────
  /projects:
    get:
      operationId: listProjects
      tags: [Projects]
      summary: List all projects
      responses:
        "200":
          description: List of projects
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Project"
        "401":
          $ref: "#/components/responses/Unauthorized"
    post:
      operationId: createProject
      tags: [Projects]
      summary: Create a project
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:
                  type: string
                  description: Project name
      responses:
        "201":
          description: Project created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Project"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /projects/{id}:
    get:
      operationId: getProject
      tags: [Projects]
      summary: Get a project
      parameters:
        - $ref: "#/components/parameters/ResourceId"
      responses:
        "200":
          description: Project details
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Project"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
    patch:
      operationId: updateProject
      tags: [Projects]
      summary: Update a project
      parameters:
        - $ref: "#/components/parameters/ResourceId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  description: Project name
                logging_mode:
                  type: string
                  enum: [full, metadata, disabled]
                  description: Request logging mode
                semantic_cache:
                  type: boolean
                  description: Enable semantic caching
      responses:
        "200":
          description: Project updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Project"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
    delete:
      operationId: deleteProject
      tags: [Projects]
      summary: Delete a project
      parameters:
        - $ref: "#/components/parameters/ResourceId"
      responses:
        "200":
          description: Project deleted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DeleteResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /projects/{id}/decisions/summary:
    get:
      operationId: getDecisionsSummary
      tags: [Analytics]
      summary: Get routing decision breakdown for a project
      description: |
        Returns aggregate routing decision data for a project: selected-model
        distribution, top skip reasons, and per-policy request counts.
      parameters:
        - $ref: "#/components/parameters/ResourceId"
        - name: from
          in: query
          schema:
            type: string
            format: date-time
          description: Start of time range (ISO 8601)
        - name: to
          in: query
          schema:
            type: string
            format: date-time
          description: End of time range (ISO 8601)
      responses:
        "200":
          description: Decision summary
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    description: Decision breakdown with model distribution, skip reasons, and policy counts
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  # ── Providers ──────────────────────────────────────────────────────
  /providers:
    get:
      operationId: listProviders
      tags: [Providers]
      summary: List provider credentials
      responses:
        "200":
          description: List of providers
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Provider"
        "401":
          $ref: "#/components/responses/Unauthorized"
    post:
      operationId: createProvider
      tags: [Providers]
      summary: Add a provider credential
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [provider, name, apiKey]
              properties:
                provider:
                  type: string
                  description: Provider type (e.g. openai, anthropic, google)
                name:
                  type: string
                  description: Display name for this credential
                apiKey:
                  type: string
                  description: Provider API key
                baseUrl:
                  type: string
                  description: Custom base URL (for Azure, self-hosted, etc.)
      responses:
        "201":
          description: Provider created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Provider"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /providers/{id}:
    get:
      operationId: getProvider
      tags: [Providers]
      summary: Get a provider credential
      parameters:
        - $ref: "#/components/parameters/ResourceId"
      responses:
        "200":
          description: Provider details
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Provider"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
    patch:
      operationId: updateProvider
      tags: [Providers]
      summary: Update a provider credential
      parameters:
        - $ref: "#/components/parameters/ResourceId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  description: Display name
                apiKey:
                  type: string
                  description: New API key
                baseUrl:
                  type: string
                  nullable: true
                  description: Custom base URL
      responses:
        "200":
          description: Provider updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Provider"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
    delete:
      operationId: deleteProvider
      tags: [Providers]
      summary: Delete a provider credential
      parameters:
        - $ref: "#/components/parameters/ResourceId"
      responses:
        "200":
          description: Provider deleted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DeleteResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  # ── API Keys ───────────────────────────────────────────────────────
  /keys:
    get:
      operationId: listApiKeys
      tags: [API Keys]
      summary: List API keys
      parameters:
        - name: projectId
          in: query
          schema:
            type: string
          description: Filter by project ID
      responses:
        "200":
          description: List of API keys
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/ApiKey"
        "401":
          $ref: "#/components/responses/Unauthorized"
    post:
      operationId: createApiKey
      tags: [API Keys]
      summary: Create an API key
      description: |
        Creates a new proxy API key. The plaintext key is returned exactly once
        in the `key` field of the response. It cannot be retrieved again.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [projectId, name]
              properties:
                projectId:
                  type: string
                  description: Project to associate the key with
                name:
                  type: string
                  description: Display name for the key
      responses:
        "201":
          description: API key created (includes plaintext key)
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/ApiKeyWithPlaintext"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /keys/{id}:
    delete:
      operationId: revokeApiKey
      tags: [API Keys]
      summary: Revoke an API key
      parameters:
        - $ref: "#/components/parameters/ResourceId"
      responses:
        "200":
          description: API key revoked
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DeleteResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  # ── Routing Configs ────────────────────────────────────────────────
  /routing-configs:
    get:
      operationId: listRoutingConfigs
      tags: [Routing Configs]
      summary: List routing configs
      parameters:
        - name: projectId
          in: query
          schema:
            type: string
          description: Filter by project ID
      responses:
        "200":
          description: List of routing configs
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/RoutingConfig"
        "401":
          $ref: "#/components/responses/Unauthorized"
    post:
      operationId: createRoutingConfig
      tags: [Routing Configs]
      summary: Create a routing config
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [projectId, name, policy, config]
              properties:
                projectId:
                  type: string
                  description: Project ID
                name:
                  type: string
                  description: Config name (used as slug for model routing)
                policy:
                  type: string
                  description: Routing policy type (e.g. single, round_robin, custom_rules)
                config:
                  type: object
                  description: Policy-specific configuration
      responses:
        "201":
          description: Routing config created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/RoutingConfig"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /routing-configs/{id}:
    get:
      operationId: getRoutingConfig
      tags: [Routing Configs]
      summary: Get a routing config
      parameters:
        - $ref: "#/components/parameters/ResourceId"
      responses:
        "200":
          description: Routing config details
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/RoutingConfig"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
    patch:
      operationId: updateRoutingConfig
      tags: [Routing Configs]
      summary: Update a routing config
      description: |
        Updates a routing config. Changes to policy or config create a new version
        automatically.
      parameters:
        - $ref: "#/components/parameters/ResourceId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                policy:
                  type: string
                config:
                  type: object
                enforce_budgets:
                  type: boolean
                is_active:
                  type: boolean
      responses:
        "200":
          description: Routing config updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/RoutingConfig"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
    delete:
      operationId: deleteRoutingConfig
      tags: [Routing Configs]
      summary: Delete a routing config
      parameters:
        - $ref: "#/components/parameters/ResourceId"
      responses:
        "200":
          description: Routing config deleted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DeleteResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /routing-configs/{id}/versions:
    get:
      operationId: listRoutingConfigVersions
      tags: [Routing Configs]
      summary: List versions of a routing config
      parameters:
        - $ref: "#/components/parameters/ResourceId"
      responses:
        "200":
          description: List of config versions
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/RoutingConfigVersion"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /routing-configs/{id}/versions/{version}:
    get:
      operationId: getRoutingConfigVersion
      tags: [Routing Configs]
      summary: Get a specific version of a routing config
      parameters:
        - $ref: "#/components/parameters/ResourceId"
        - name: version
          in: path
          required: true
          schema:
            type: integer
            minimum: 1
          description: Version number
      responses:
        "200":
          description: Config version details
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/RoutingConfigVersion"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /routing-configs/{id}/restore:
    post:
      operationId: restoreRoutingConfigVersion
      tags: [Routing Configs]
      summary: Restore a previous version
      description: |
        Restores a routing config to a previous version. Creates a new version
        with the restored config.
      parameters:
        - $ref: "#/components/parameters/ResourceId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [version]
              properties:
                version:
                  type: integer
                  minimum: 1
                  description: Version number to restore
      responses:
        "200":
          description: Config restored
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/RoutingConfig"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /routing-configs/dry-run:
    post:
      operationId: dryRunRoutingConfig
      tags: [Routing Configs]
      summary: Test a routing config with a dry-run
      description: |
        Forwards a test request to the proxy's dry-run endpoint to evaluate
        routing decisions without making an actual LLM call. Validates the
        policy config locally before forwarding.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [project_id, policy, config]
              properties:
                project_id:
                  type: string
                  description: Project ID to run the dry-run in
                policy:
                  type: string
                  description: Routing policy type
                config:
                  type: object
                  description: Policy configuration to test
                request:
                  type: object
                  description: Sample completion request to route
                tags:
                  type: object
                  additionalProperties:
                    type: string
                  description: Tags to attach to the request
                end_user_id:
                  type: string
                  description: End user identifier
                trace_id:
                  type: string
                  description: Trace ID
      responses:
        "200":
          description: Dry-run result from the proxy
          content:
            application/json:
              schema:
                type: object
                description: Proxy dry-run response with routing decision details
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "502":
          description: Proxy unreachable
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  # ── Logs ───────────────────────────────────────────────────────────
  /logs:
    get:
      operationId: listLogs
      tags: [Logs]
      summary: List request logs
      parameters:
        - name: projectId
          in: query
          schema:
            type: string
          description: Filter by project ID
        - name: endUserId
          in: query
          schema:
            type: string
          description: Filter by end user ID
        - name: status
          in: query
          schema:
            type: string
            enum: [success, error]
          description: Filter by request status
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
          description: Number of results to return
        - name: offset
          in: query
          schema:
            type: integer
            default: 0
          description: Number of results to skip
      responses:
        "200":
          description: List of request logs
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Log"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /logs/{id}:
    get:
      operationId: getLog
      tags: [Logs]
      summary: Get log detail
      parameters:
        - $ref: "#/components/parameters/ResourceId"
      responses:
        "200":
          description: Log details
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Log"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /logs/{id}/trace:
    get:
      operationId: getDecisionTrace
      tags: [Logs]
      summary: Get the decision trace for a logged request
      description: |
        Returns the structured routing decision trace explaining why a
        particular model/provider was selected for this request.
      parameters:
        - $ref: "#/components/parameters/ResourceId"
      responses:
        "200":
          description: Decision trace
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/DecisionTrace"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /logs/{id}/replay:
    post:
      operationId: replayLog
      tags: [Logs]
      summary: Replay a logged request as a dry-run
      description: |
        Reconstructs the original request from the log and replays it through
        the current routing config as a dry-run. No LLM call is made. Returns
        what routing decision the current config would make for this exact request.
      parameters:
        - $ref: "#/components/parameters/ResourceId"
      responses:
        "200":
          description: Replay result from the proxy
          content:
            application/json:
              schema:
                type: object
                description: Proxy dry-run response showing the routing decision
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "502":
          description: Proxy unreachable
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  # ── Analytics ──────────────────────────────────────────────────────
  /analytics:
    get:
      operationId: getAnalytics
      tags: [Analytics]
      summary: Get analytics report
      description: |
        Returns a full analytics report including request counts, latency,
        token usage, cost, error rates, and time-series data.
      parameters:
        - name: projectId
          in: query
          schema:
            type: string
          description: Filter by project ID
        - name: startTime
          in: query
          schema:
            type: string
            format: date-time
          description: Start of time range (ISO 8601)
        - name: endTime
          in: query
          schema:
            type: string
            format: date-time
          description: End of time range (ISO 8601)
        - name: bucket
          in: query
          schema:
            type: string
            enum: [hour, day]
          description: Time-series bucket size
      responses:
        "200":
          description: Analytics report
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/AnalyticsReport"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"

  # ── Budgets ────────────────────────────────────────────────────────
  /budgets:
    get:
      operationId: listBudgets
      tags: [Budgets]
      summary: List budgets
      parameters:
        - name: projectId
          in: query
          schema:
            type: string
          description: Filter by project ID
        - name: scopeType
          in: query
          schema:
            type: string
            enum: [org, project, end_user]
          description: Filter by scope type
      responses:
        "200":
          description: List of budgets
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Budget"
        "401":
          $ref: "#/components/responses/Unauthorized"
    post:
      operationId: createBudget
      tags: [Budgets]
      summary: Create a budget
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, scope_type, period, amount_usd]
              properties:
                name:
                  type: string
                  description: Budget name
                scope_type:
                  type: string
                  enum: [org, project, end_user]
                  description: What the budget applies to
                scope_id:
                  type: string
                  nullable: true
                  description: ID of the scoped entity (project ID, user ID, etc.)
                period:
                  type: string
                  enum: [daily, weekly, monthly]
                  description: Budget reset period
                amount_usd:
                  type: string
                  description: Budget amount in USD (decimal string)
                soft_cap_pct:
                  type: number
                  minimum: 0
                  maximum: 100
                  description: Percentage at which soft cap alerts fire
                hard_cap_action:
                  type: string
                  enum: [block, downgrade]
                  description: Action when hard cap is hit
                downgrade_to:
                  type: string
                  nullable: true
                  description: Model to downgrade to (when hard_cap_action is downgrade)
      responses:
        "201":
          description: Budget created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Budget"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /budgets/{id}:
    get:
      operationId: getBudget
      tags: [Budgets]
      summary: Get a budget with current spend
      parameters:
        - $ref: "#/components/parameters/ResourceId"
      responses:
        "200":
          description: Budget details
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Budget"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
    patch:
      operationId: updateBudget
      tags: [Budgets]
      summary: Update a budget
      parameters:
        - $ref: "#/components/parameters/ResourceId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                amount_usd:
                  type: string
                soft_cap_pct:
                  type: number
                hard_cap_action:
                  type: string
                  enum: [block, downgrade]
                downgrade_to:
                  type: string
                  nullable: true
                active:
                  type: boolean
      responses:
        "200":
          description: Budget updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Budget"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
    delete:
      operationId: deleteBudget
      tags: [Budgets]
      summary: Delete a budget
      parameters:
        - $ref: "#/components/parameters/ResourceId"
      responses:
        "200":
          description: Budget deleted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DeleteResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /budgets/{id}/reset:
    post:
      operationId: resetBudgetSpend
      tags: [Budgets]
      summary: Reset the spend counter for a budget
      parameters:
        - $ref: "#/components/parameters/ResourceId"
      responses:
        "200":
          description: Spend counter reset
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    description: Reset confirmation
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /budgets/{id}/events:
    get:
      operationId: listBudgetEvents
      tags: [Budgets]
      summary: List budget events
      description: Returns events for a budget (soft cap warnings, hard cap triggers, resets).
      parameters:
        - $ref: "#/components/parameters/ResourceId"
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
          description: Number of events to return
      responses:
        "200":
          description: List of budget events
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/BudgetEvent"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /budgets/{id}/alerts:
    get:
      operationId: listBudgetAlerts
      tags: [Budgets]
      summary: List alerts for a budget
      parameters:
        - $ref: "#/components/parameters/ResourceId"
      responses:
        "200":
          description: List of alerts
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/BudgetAlert"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
    post:
      operationId: createBudgetAlert
      tags: [Budgets]
      summary: Create a budget alert
      parameters:
        - $ref: "#/components/parameters/ResourceId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [threshold_pct, channel]
              properties:
                threshold_pct:
                  type: number
                  minimum: 0
                  maximum: 100
                  description: Percentage threshold to trigger the alert
                channel:
                  type: string
                  enum: [email, webhook]
                  description: Alert delivery channel
                destination:
                  type: string
                  description: Email address or webhook URL
      responses:
        "201":
          description: Alert created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/BudgetAlert"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
    delete:
      operationId: deleteBudgetAlert
      tags: [Budgets]
      summary: Delete a budget alert
      parameters:
        - $ref: "#/components/parameters/ResourceId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [alertId]
              properties:
                alertId:
                  type: string
                  description: ID of the alert to delete
      responses:
        "200":
          description: Alert deleted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DeleteResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  # ── Webhooks ───────────────────────────────────────────────────────
  /webhooks/event-types:
    get:
      operationId: listWebhookEventTypes
      tags: [Webhooks]
      summary: List available webhook event types
      description: |
        Returns the registry of event types that webhook endpoints can subscribe
        to. No authentication required.
      security: []
      responses:
        "200":
          description: List of event types
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/WebhookEventType"

  /webhooks/endpoints:
    get:
      operationId: listWebhookEndpoints
      tags: [Webhooks]
      summary: List webhook endpoints
      parameters:
        - name: projectId
          in: query
          schema:
            type: string
            nullable: true
          description: Filter by project ID. Pass "null" for org-scoped endpoints only.
      responses:
        "200":
          description: List of webhook endpoints
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/WebhookEndpoint"
        "401":
          $ref: "#/components/responses/Unauthorized"
    post:
      operationId: createWebhookEndpoint
      tags: [Webhooks]
      summary: Create a webhook endpoint
      description: |
        Creates a new webhook endpoint. The `signing_secret` is returned exactly
        once in the response and cannot be retrieved again.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, url, event_types]
              properties:
                name:
                  type: string
                  description: Endpoint display name
                url:
                  type: string
                  format: uri
                  description: Webhook delivery URL
                project_id:
                  type: string
                  nullable: true
                  description: Scope to a project (null for org-wide)
                event_types:
                  type: array
                  items:
                    type: string
                  description: Event types to subscribe to
      responses:
        "201":
          description: Endpoint created (includes signing secret)
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      endpoint:
                        $ref: "#/components/schemas/WebhookEndpoint"
                      signingSecret:
                        type: string
                        description: HMAC signing secret (shown once)
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /webhooks/endpoints/{id}:
    get:
      operationId: getWebhookEndpoint
      tags: [Webhooks]
      summary: Get a webhook endpoint
      parameters:
        - $ref: "#/components/parameters/ResourceId"
      responses:
        "200":
          description: Endpoint details
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/WebhookEndpoint"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
    patch:
      operationId: updateWebhookEndpoint
      tags: [Webhooks]
      summary: Update a webhook endpoint
      parameters:
        - $ref: "#/components/parameters/ResourceId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                url:
                  type: string
                  format: uri
                event_types:
                  type: array
                  items:
                    type: string
                active:
                  type: boolean
      responses:
        "200":
          description: Endpoint updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/WebhookEndpoint"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
    delete:
      operationId: deleteWebhookEndpoint
      tags: [Webhooks]
      summary: Delete a webhook endpoint
      parameters:
        - $ref: "#/components/parameters/ResourceId"
      responses:
        "200":
          description: Endpoint deleted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DeleteResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /webhooks/endpoints/{id}/rotate:
    post:
      operationId: rotateWebhookSecret
      tags: [Webhooks]
      summary: Rotate the signing secret
      description: |
        Generates a new signing secret for the endpoint. The old secret stops
        working immediately. The new secret is returned once.
      parameters:
        - $ref: "#/components/parameters/ResourceId"
      responses:
        "200":
          description: New signing secret
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      signing_secret:
                        type: string
                        description: New HMAC signing secret
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /webhooks/endpoints/{id}/test:
    post:
      operationId: testWebhookEndpoint
      tags: [Webhooks]
      summary: Send a test event to the endpoint
      description: |
        Fires a synthetic `webhook.test` event. The delivery uses the same
        envelope and signature flow as real events, with `data.synthetic = true`.
      parameters:
        - $ref: "#/components/parameters/ResourceId"
      responses:
        "202":
          description: Test event enqueued
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    description: Test delivery details
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /webhooks/endpoints/{id}/deliveries:
    get:
      operationId: listWebhookDeliveries
      tags: [Webhooks]
      summary: List deliveries for an endpoint
      parameters:
        - $ref: "#/components/parameters/ResourceId"
      responses:
        "200":
          description: List of deliveries
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/WebhookDelivery"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /webhooks/deliveries/{id}:
    get:
      operationId: getWebhookDelivery
      tags: [Webhooks]
      summary: Get a delivery
      parameters:
        - $ref: "#/components/parameters/ResourceId"
      responses:
        "200":
          description: Delivery details
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/WebhookDelivery"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /webhooks/deliveries/{id}/replay:
    post:
      operationId: replayWebhookDelivery
      tags: [Webhooks]
      summary: Replay a failed delivery
      description: |
        Re-enqueues a failed delivery for retry. Resets the attempt count and
        schedules the next attempt immediately.
      parameters:
        - $ref: "#/components/parameters/ResourceId"
      responses:
        "200":
          description: Delivery re-enqueued
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/WebhookDelivery"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  # ── Simulations ────────────────────────────────────────────────────
  /simulations:
    get:
      operationId: listSimulations
      tags: [Simulations]
      summary: List simulations
      parameters:
        - name: projectId
          in: query
          schema:
            type: string
          description: Filter by project ID
      responses:
        "200":
          description: List of simulations
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Simulation"
        "401":
          $ref: "#/components/responses/Unauthorized"
    post:
      operationId: createSimulation
      tags: [Simulations]
      summary: Create a simulation
      description: |
        Creates a new routing simulation that replays historical traffic against
        a candidate routing config and compares results to the baseline.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, candidate_policy, candidate_config, window_start, window_end]
              properties:
                name:
                  type: string
                  description: Simulation name
                project_id:
                  type: string
                  nullable: true
                  description: Project to simulate against
                candidate_policy:
                  type: string
                  description: Routing policy to test
                candidate_config:
                  type: object
                  description: Candidate routing config
                baseline_config_id:
                  type: string
                  nullable: true
                  description: Existing routing config to compare against
                window_start:
                  type: string
                  format: date-time
                  description: Start of the historical traffic window
                window_end:
                  type: string
                  format: date-time
                  description: End of the historical traffic window
                filter:
                  type: object
                  nullable: true
                  description: Optional filter criteria for selecting traffic
      responses:
        "201":
          description: Simulation created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Simulation"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /simulations/{id}:
    get:
      operationId: getSimulation
      tags: [Simulations]
      summary: Get a simulation
      parameters:
        - $ref: "#/components/parameters/ResourceId"
      responses:
        "200":
          description: Simulation details
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Simulation"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /simulations/{id}/cancel:
    post:
      operationId: cancelSimulation
      tags: [Simulations]
      summary: Cancel a running simulation
      parameters:
        - $ref: "#/components/parameters/ResourceId"
      responses:
        "200":
          description: Simulation cancelled
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Simulation"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /simulations/{id}/promote:
    post:
      operationId: promoteSimulation
      tags: [Simulations]
      summary: Promote simulation candidate to production
      description: |
        Applies the candidate routing config from a completed simulation to
        the production routing config for the project.
      parameters:
        - $ref: "#/components/parameters/ResourceId"
      responses:
        "200":
          description: Candidate promoted
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    description: Promotion result
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /simulations/{id}/results:
    get:
      operationId: getSimulationResults
      tags: [Simulations]
      summary: Get per-request simulation results
      parameters:
        - $ref: "#/components/parameters/ResourceId"
      responses:
        "200":
          description: Per-request results
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/SimulationResult"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /simulations/estimate:
    get:
      operationId: estimateSimulation
      tags: [Simulations]
      summary: Estimate request count for a simulation window
      parameters:
        - name: projectId
          in: query
          required: true
          schema:
            type: string
          description: Project ID
        - name: from
          in: query
          required: true
          schema:
            type: string
            format: date-time
          description: Window start (ISO 8601)
        - name: to
          in: query
          required: true
          schema:
            type: string
            format: date-time
          description: Window end (ISO 8601)
      responses:
        "200":
          description: Estimated request count
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      request_count:
                        type: integer
                        description: Estimated number of requests in the window
        "401":
          $ref: "#/components/responses/Unauthorized"

  # ── Audit Logs ─────────────────────────────────────────────────────
  /audit-logs:
    get:
      operationId: listAuditLogs
      tags: [Audit Logs]
      summary: List audit logs
      parameters:
        - name: resourceType
          in: query
          schema:
            type: string
          description: Filter by resource type (e.g. project, routing_config, budget)
        - name: resourceId
          in: query
          schema:
            type: string
          description: Filter by resource ID
        - name: action
          in: query
          schema:
            type: string
          description: Filter by action (e.g. created, updated, deleted)
        - name: actorId
          in: query
          schema:
            type: string
          description: Filter by actor user ID
        - name: since
          in: query
          schema:
            type: string
            format: date-time
          description: Start of time range (ISO 8601)
        - name: until
          in: query
          schema:
            type: string
            format: date-time
          description: End of time range (ISO 8601)
        - name: limit
          in: query
          schema:
            type: integer
          description: Number of results to return
        - name: offset
          in: query
          schema:
            type: integer
          description: Number of results to skip
      responses:
        "200":
          description: List of audit logs
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/AuditLog"
                  total:
                    type: integer
                    description: Total number of matching audit log entries
        "401":
          $ref: "#/components/responses/Unauthorized"

  /audit-logs/{id}:
    get:
      operationId: getAuditLog
      tags: [Audit Logs]
      summary: Get an audit log entry
      parameters:
        - $ref: "#/components/parameters/ResourceId"
      responses:
        "200":
          description: Audit log entry
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/AuditLog"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  # ── Management Keys ────────────────────────────────────────────────
  /management-keys:
    get:
      operationId: listManagementKeys
      tags: [Management Keys]
      summary: List management API keys
      responses:
        "200":
          description: List of management keys
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/ManagementKey"
        "401":
          $ref: "#/components/responses/Unauthorized"
    post:
      operationId: createManagementKey
      tags: [Management Keys]
      summary: Create a management API key
      description: |
        Creates a new management API key. The plaintext key is returned exactly
        once in the `key` field and cannot be retrieved again.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:
                  type: string
                  description: Display name for the key
      responses:
        "201":
          description: Management key created (includes plaintext key)
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/ManagementKeyWithPlaintext"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /management-keys/{id}:
    delete:
      operationId: revokeManagementKey
      tags: [Management Keys]
      summary: Revoke a management API key
      parameters:
        - $ref: "#/components/parameters/ResourceId"
      responses:
        "200":
          description: Management key revoked
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DeleteResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      description: |
        Management API key with `mlx_mgmt_` prefix, or a session cookie from
        the dashboard. Pass as `Authorization: Bearer mlx_mgmt_...`.

  parameters:
    ResourceId:
      name: id
      in: path
      required: true
      schema:
        type: string
      description: Resource ID

    OrgId:
      name: id
      in: path
      required: true
      schema:
        type: string
      description: Organization ID

    UserId:
      name: userId
      in: path
      required: true
      schema:
        type: string
      description: User ID

  responses:
    BadRequest:
      description: Invalid request
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"

    Unauthorized:
      description: Authentication required or insufficient permissions
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"

    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"

  schemas:
    # ── Error ──────────────────────────────────────────────────────────
    Error:
      type: object
      required: [error, code]
      properties:
        error:
          type: string
          description: Human-readable error message
        code:
          type: string
          description: Machine-readable error code

    # ── Delete Response ────────────────────────────────────────────────
    DeleteResponse:
      type: object
      properties:
        data:
          type: object
          required: [id, deleted]
          properties:
            id:
              type: string
            deleted:
              type: boolean
              const: true

    # ── Organization ───────────────────────────────────────────────────
    Org:
      type: object
      required: [id, name, slug, plan, created_at]
      properties:
        id:
          type: string
        name:
          type: string
        slug:
          type: string
        plan:
          type: string
          description: Billing plan (e.g. free, pro, enterprise)
        billing_email:
          type: string
          nullable: true
          format: email
        created_at:
          type: string
          format: date-time

    # ── Member ─────────────────────────────────────────────────────────
    Member:
      type: object
      required: [user_id, role, created_at, user]
      properties:
        user_id:
          type: string
        role:
          type: string
          enum: [owner, admin, member]
        created_at:
          type: string
          format: date-time
        user:
          type: object
          required: [id, name, email, image]
          properties:
            id:
              type: string
            name:
              type: string
              nullable: true
            email:
              type: string
              nullable: true
            image:
              type: string
              nullable: true

    # ── Project ────────────────────────────────────────────────────────
    Project:
      type: object
      required: [id, name, slug, settings, created_at]
      properties:
        id:
          type: string
        name:
          type: string
        slug:
          type: string
        settings:
          type: object
          description: Project settings (logging_mode, semantic_cache, etc.)
        created_at:
          type: string
          format: date-time
        api_key_count:
          type: integer
          description: Number of API keys (included in list responses)
        routing_config_count:
          type: integer
          description: Number of routing configs (included in list responses)

    # ── API Key ────────────────────────────────────────────────────────
    ApiKey:
      type: object
      required: [id, key_prefix, name, created_at]
      properties:
        id:
          type: string
        key_prefix:
          type: string
          description: First characters of the key (e.g. mlx_sk_abc...)
        name:
          type: string
        created_at:
          type: string
          format: date-time
        last_used_at:
          type: string
          nullable: true
          format: date-time
        revoked_at:
          type: string
          nullable: true
          format: date-time
        project:
          type: object
          nullable: true
          properties:
            id:
              type: string
            name:
              type: string

    ApiKeyWithPlaintext:
      allOf:
        - $ref: "#/components/schemas/ApiKey"
        - type: object
          required: [key]
          properties:
            key:
              type: string
              description: Full plaintext API key (shown once at creation)

    # ── Provider ───────────────────────────────────────────────────────
    Provider:
      type: object
      required: [id, provider, name, is_default, created_at]
      properties:
        id:
          type: string
        provider:
          type: string
          description: Provider type (openai, anthropic, google, etc.)
        name:
          type: string
          description: Display name
        base_url:
          type: string
          nullable: true
          description: Custom base URL
        is_default:
          type: boolean
          description: Whether this is the default credential for the provider type
        created_at:
          type: string
          format: date-time
        last_verified_at:
          type: string
          nullable: true
          format: date-time
          description: Last time the credential was verified as working

    # ── Routing Config ─────────────────────────────────────────────────
    RoutingConfig:
      type: object
      required: [id, name, project_id, policy, config, created_at]
      properties:
        id:
          type: string
        name:
          type: string
        project_id:
          type: string
        policy:
          type: string
          description: Routing policy type (single, round_robin, custom_rules, etc.)
        config:
          type: object
          description: Policy-specific configuration
        version:
          type: integer
          description: Current version number
        enforce_budgets:
          type: boolean
          description: Whether budget enforcement is enabled
        is_active:
          type: boolean
          description: Whether this config is active
        created_at:
          type: string
          format: date-time

    RoutingConfigVersion:
      type: object
      required: [version, policy, config, created_at]
      properties:
        version:
          type: integer
        policy:
          type: string
        config:
          type: object
        created_at:
          type: string
          format: date-time
        created_by:
          type: string
          nullable: true
          description: Actor who created this version

    # ── Budget ─────────────────────────────────────────────────────────
    Budget:
      type: object
      required: [id, name, scope_type, period, amount_usd, soft_cap_pct, hard_cap_action, active, created_at]
      properties:
        id:
          type: string
        name:
          type: string
        scope_type:
          type: string
          enum: [org, project, end_user]
        scope_id:
          type: string
          nullable: true
        period:
          type: string
          enum: [daily, weekly, monthly]
        amount_usd:
          type: string
          description: Budget amount as a decimal string (e.g. "100.00")
        soft_cap_pct:
          type: number
          description: Percentage threshold for soft cap alerts
        hard_cap_action:
          type: string
          enum: [block, downgrade]
        downgrade_to:
          type: string
          nullable: true
          description: Model to downgrade to when hard cap action is downgrade
        active:
          type: boolean
        created_at:
          type: string
          format: date-time

    BudgetEvent:
      type: object
      required: [id, budget_id, event_type, occurred_at]
      properties:
        id:
          type: string
        budget_id:
          type: string
        event_type:
          type: string
          description: Event type (soft_cap_reached, hard_cap_triggered, reset, etc.)
        details:
          type: object
          nullable: true
          description: Event-specific details
        occurred_at:
          type: string
          format: date-time

    BudgetAlert:
      type: object
      required: [id, budget_id, threshold_pct, channel]
      properties:
        id:
          type: string
        budget_id:
          type: string
        threshold_pct:
          type: number
        channel:
          type: string
          enum: [email, webhook]
        destination:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time

    # ── Webhook ────────────────────────────────────────────────────────
    WebhookEventType:
      type: object
      required: [type, description]
      properties:
        type:
          type: string
          description: Event type identifier (e.g. budget.soft_cap, request.error)
        description:
          type: string
          description: Human-readable description

    WebhookEndpoint:
      type: object
      required: [id, name, url, event_types, active, created_at]
      properties:
        id:
          type: string
        name:
          type: string
        url:
          type: string
          format: uri
        project_id:
          type: string
          nullable: true
          description: Scoped to this project, or null for org-wide
        event_types:
          type: array
          items:
            type: string
        active:
          type: boolean
        created_at:
          type: string
          format: date-time

    WebhookDelivery:
      type: object
      required: [id, endpoint_id, event_type, status]
      properties:
        id:
          type: string
        endpoint_id:
          type: string
        event_type:
          type: string
        status:
          type: string
          enum: [pending, delivered, failed, dead]
        payload:
          type: object
          description: Event payload that was delivered
        response_status:
          type: integer
          nullable: true
          description: HTTP status code from the receiver
        response_body:
          type: string
          nullable: true
          description: Response body from the receiver
        attempt_count:
          type: integer
        next_attempt_at:
          type: string
          nullable: true
          format: date-time
        created_at:
          type: string
          format: date-time
        delivered_at:
          type: string
          nullable: true
          format: date-time

    # ── Simulation ─────────────────────────────────────────────────────
    Simulation:
      type: object
      required: [id, name, candidate_policy, candidate_config, window_start, window_end, status, request_count, processed_count, created_at]
      properties:
        id:
          type: string
        name:
          type: string
        project_id:
          type: string
          nullable: true
        candidate_policy:
          type: string
          description: Routing policy being tested
        candidate_config:
          type: object
          description: Candidate routing configuration
        baseline_config_id:
          type: string
          nullable: true
          description: ID of the baseline routing config for comparison
        window_start:
          type: string
          format: date-time
        window_end:
          type: string
          format: date-time
        filter:
          type: object
          nullable: true
          description: Traffic filter criteria
        status:
          type: string
          enum: [pending, running, completed, cancelled, failed]
        request_count:
          type: integer
          description: Total requests in the window
        processed_count:
          type: integer
          description: Requests processed so far
        baseline_summary:
          type: object
          nullable: true
          description: Aggregate results from baseline routing
        candidate_summary:
          type: object
          nullable: true
          description: Aggregate results from candidate routing
        error_message:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
        completed_at:
          type: string
          nullable: true
          format: date-time
        cancelled_at:
          type: string
          nullable: true
          format: date-time

    SimulationResult:
      type: object
      required: [request_id]
      properties:
        request_id:
          type: string
        baseline_decision:
          type: object
          nullable: true
          description: Routing decision under the baseline config
        candidate_decision:
          type: object
          nullable: true
          description: Routing decision under the candidate config
        diff:
          type: object
          nullable: true
          description: Differences between baseline and candidate

    # ── Log ────────────────────────────────────────────────────────────
    Log:
      type: object
      required: [id, project_id, timestamp]
      properties:
        id:
          type: string
        project_id:
          type: string
        timestamp:
          type: string
          format: date-time
        status:
          type: string
          enum: [success, error]
        provider:
          type: string
          nullable: true
          description: Provider used (openai, anthropic, etc.)
        model_requested:
          type: string
          nullable: true
        model_used:
          type: string
          nullable: true
        routing_config_name:
          type: string
          nullable: true
        latency_ms:
          type: number
          nullable: true
        input_tokens:
          type: integer
          nullable: true
        output_tokens:
          type: integer
          nullable: true
        total_tokens:
          type: integer
          nullable: true
        cost_usd:
          type: string
          nullable: true
          description: Request cost as decimal string
        end_user_id:
          type: string
          nullable: true
        trace_id:
          type: string
          nullable: true
        tags:
          type: object
          nullable: true
          additionalProperties:
            type: string
        error_message:
          type: string
          nullable: true
        request_messages:
          type: array
          nullable: true
          items:
            type: object
          description: Request messages (when logging_mode is full)

    DecisionTrace:
      type: object
      description: Structured routing decision trace explaining model/provider selection
      properties:
        policy:
          type: string
          description: Policy that was evaluated
        selected_model:
          type: string
          description: Model that was selected
        selected_provider:
          type: string
          description: Provider that was selected
        steps:
          type: array
          items:
            type: object
            properties:
              rule:
                type: string
              result:
                type: string
              reason:
                type: string
          description: Evaluation steps showing rule matches and skips

    # ── Audit Log ──────────────────────────────────────────────────────
    AuditLog:
      type: object
      required: [id, org_id, actor_id, resource_type, resource_id, action, occurred_at]
      properties:
        id:
          type: string
        org_id:
          type: string
        actor_id:
          type: string
        actor_email:
          type: string
          nullable: true
        resource_type:
          type: string
          description: Type of resource (project, routing_config, budget, api_key, etc.)
        resource_id:
          type: string
        action:
          type: string
          description: Action performed (created, updated, deleted, etc.)
        changes:
          type: object
          nullable: true
          description: Before/after diff of changed fields
        metadata:
          type: object
          nullable: true
          description: Additional context
        occurred_at:
          type: string
          format: date-time

    # ── Management Key ─────────────────────────────────────────────────
    ManagementKey:
      type: object
      required: [id, name, key_prefix, created_at]
      properties:
        id:
          type: string
        name:
          type: string
        key_prefix:
          type: string
          description: First characters of the key (e.g. mlx_mgmt_abc...)
        created_at:
          type: string
          format: date-time
        last_used_at:
          type: string
          nullable: true
          format: date-time
        revoked_at:
          type: string
          nullable: true
          format: date-time

    ManagementKeyWithPlaintext:
      allOf:
        - $ref: "#/components/schemas/ManagementKey"
        - type: object
          required: [key]
          properties:
            key:
              type: string
              description: Full plaintext management key (shown once at creation)

    # ── Analytics ──────────────────────────────────────────────────────
    AnalyticsReport:
      type: object
      properties:
        total_requests:
          type: integer
        total_tokens:
          type: integer
        total_cost_usd:
          type: string
          description: Total cost as decimal string
        avg_latency_ms:
          type: number
        error_rate:
          type: number
          description: Error rate as a fraction (0.0 - 1.0)
        by_provider:
          type: array
          items:
            type: object
            properties:
              provider:
                type: string
              request_count:
                type: integer
              total_tokens:
                type: integer
              total_cost_usd:
                type: string
              avg_latency_ms:
                type: number
        by_model:
          type: array
          items:
            type: object
            properties:
              model:
                type: string
              request_count:
                type: integer
              total_tokens:
                type: integer
              total_cost_usd:
                type: string
              avg_latency_ms:
                type: number
        time_series:
          type: array
          items:
            type: object
            properties:
              bucket:
                type: string
                format: date-time
              request_count:
                type: integer
              total_tokens:
                type: integer
              total_cost_usd:
                type: string
              avg_latency_ms:
                type: number
              error_count:
                type: integer
