openapi: 3.0.3
info:
  title: GPTBased Leaderboard
  version: "1.0.0"
  description: |
    Combined LMArena Elo rankings and OpenRouter pricing across 5 categories
    (text, webdev, vision, text-to-image, image-edit). Adds daily historical
    snapshots, weekly movers, per-model detail with provider endpoints, and
    Wavespeed inference URLs for image and video variants.

    All endpoints are read-only and return JSON. Cache TTL is 12 hours
    (24 hours on /meta). Snapshots refresh daily; rankings stay stable
    intra-day.

servers:
  - url: https://gptbased.com
    description: Production origin (RapidAPI routes here)

tags:
  - name: Discovery
    description: Surface metadata
  - name: Browse
    description: Paginated lists
  - name: Picks
    description: Curated recommendations
  - name: Detail
    description: Single-model lookups
  - name: Trends
    description: Historical snapshots and movement

paths:

  /api/rapid/v1/meta:
    get:
      tags: [Discovery]
      summary: List supported categories, slugs, and limits
      description: |
        Returns the valid `category` values, `best_for` slugs, universal
        request-size caps, and the full endpoint catalog. Call this first
        from any client to discover what the API supports without burning
        quota on intentional 400 responses.
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MetaResponse"

  /api/rapid/v1/leaderboard:
    get:
      tags: [Browse]
      summary: Paginated leaderboard for a category
      description: |
        Returns ranked models for one category. Includes OpenRouter-matched
        rows (with `slug`) and arena-only rows (with `slug: null`). The
        `free` filter excludes arena-only rows since they have no pricing
        data to mark as free vs paid.
      parameters:
        - $ref: "#/components/parameters/Category"
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Offset"
        - name: free
          in: query
          description: |
            `true` keeps only OpenRouter `:free` variants. `false` keeps only
            paid OR variants (excludes arena-only). Omit for everything.
          required: false
          schema:
            type: boolean
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LeaderboardResponse"
        "400":
          $ref: "#/components/responses/BadRequest"

  /api/rapid/v1/picks:
    get:
      tags: [Picks]
      summary: Editorial picks for a category
      description: |
        Returns three picks for one category: `top_ranked` (highest Elo
        OR-matched), `best_value` (Pareto knee in cost/rating space), and
        `recommended` (highest-rated frontier point at-or-below median
        frontier cost). Same algorithm the marketing leaderboard uses.
      parameters:
        - $ref: "#/components/parameters/Category"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PicksResponse"
        "400":
          $ref: "#/components/responses/BadRequest"

  /api/rapid/v1/search:
    get:
      tags: [Browse]
      summary: Filtered leaderboard query
      description: |
        Filter the leaderboard by combinations of Elo, pricing, context
        length, and modalities. All filters AND-combine. Arena-only rows are
        excluded (no pricing or context_length to filter on).
      parameters:
        - $ref: "#/components/parameters/Category"
        - name: free
          in: query
          schema: { type: boolean }
        - name: min_elo
          in: query
          schema: { type: number, example: 1300 }
        - name: max_prompt_per_m_usd
          in: query
          description: Cap on prompt-token price ($/M tokens).
          schema: { type: number, example: 2 }
        - name: max_completion_per_m_usd
          in: query
          schema: { type: number }
        - name: min_context_length
          in: query
          schema: { type: integer, example: 200000 }
        - name: input_modality
          in: query
          description: Single modality token, e.g. `image`, `file`, `audio`.
          schema: { type: string, example: image }
        - name: output_modality
          in: query
          schema: { type: string }
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Offset"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SearchResponse"
        "400":
          $ref: "#/components/responses/BadRequest"

  /api/rapid/v1/compare:
    get:
      tags: [Detail]
      summary: Side-by-side comparison for multiple OpenRouter ids
      description: |
        Returns identity, modalities, pricing, and every overall placement
        for each requested OR id. Preserves requested order so a comparison
        UI can render a wide table without re-sorting.
      parameters:
        - name: ids
          in: query
          required: true
          description: |
            Comma-separated OpenRouter model ids. Capped at 10; extras are
            silently truncated and `truncated: true` is set on the response.
          schema:
            type: string
            example: anthropic/claude-opus-4.6,openai/gpt-5
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CompareResponse"
        "400":
          $ref: "#/components/responses/BadRequest"

  /api/rapid/v1/model:
    get:
      tags: [Detail]
      summary: Full detail for one OpenRouter model
      description: |
        Returns identity, modalities, pricing, every leaderboard placement
        (subset, category, variant, harness, rank, Elo), and the per-provider
        endpoint list with uptime, latency, throughput, and regional pricing.

        `id` is a query param (not a path segment) because OR ids contain
        slashes.
      parameters:
        - name: id
          in: query
          required: true
          description: OpenRouter model id (e.g. `anthropic/claude-opus-4.6`).
          schema:
            type: string
            example: anthropic/claude-opus-4.6
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ModelDetailResponse"
        "404":
          $ref: "#/components/responses/NotFound"

  /api/rapid/v1/history:
    get:
      tags: [Trends]
      summary: Daily rank and Elo history for one model
      description: |
        One series per (variant, harness) pair so reasoning modes stay
        separated. `days` is capped at 365.
      parameters:
        - name: id
          in: query
          required: true
          schema:
            type: string
            example: anthropic/claude-opus-4.6
        - name: category
          in: query
          required: false
          description: Defaults to `text`.
          schema:
            $ref: "#/components/schemas/Category"
        - $ref: "#/components/parameters/Days"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HistoryResponse"
        "404":
          $ref: "#/components/responses/NotFound"

  /api/rapid/v1/movers:
    get:
      tags: [Trends]
      summary: Top climbers and fallers between snapshot dates
      description: |
        Computes the rank delta between the earliest snapshot taken on or
        after `days` ago and the latest snapshot. Climber delta is positive
        (rank number went down = up the leaderboard).
      parameters:
        - $ref: "#/components/parameters/Category"
        - $ref: "#/components/parameters/Days"
        - $ref: "#/components/parameters/Limit"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MoversResponse"
        "400":
          $ref: "#/components/responses/BadRequest"

  /api/rapid/v1/best_for/{slug}:
    get:
      tags: [Picks]
      summary: Use-case-tailored picks
      description: |
        Pre-baked picks tuned for a use case. `coding` is the top of
        text/coding category; `multilingual` averages rank across language
        subsets; `cheap` returns the Pareto frontier by cost.
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
            enum: [coding, multilingual, cheap]
        - $ref: "#/components/parameters/Limit"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BestForResponse"
        "400":
          $ref: "#/components/responses/BadRequest"

components:
  parameters:
    Category:
      name: category
      in: query
      required: true
      schema:
        $ref: "#/components/schemas/Category"
    Limit:
      name: limit
      in: query
      description: Hard-capped at 500.
      schema:
        type: integer
        minimum: 1
        maximum: 500
        default: 10
    Offset:
      name: offset
      in: query
      schema:
        type: integer
        minimum: 0
        default: 0
    Days:
      name: days
      in: query
      description: Hard-capped at 365.
      schema:
        type: integer
        minimum: 1
        maximum: 365
        default: 30

  schemas:

    Category:
      type: string
      enum: [text, webdev, vision, text_to_image, image_edit]

    LeaderboardRow:
      type: object
      properties:
        rank: { type: integer, example: 1 }
        name: { type: string, example: Claude Opus 4.6 }
        slug:
          type: string
          nullable: true
          description: OpenRouter id, or null for arena-only rows.
          example: anthropic/claude-opus-4.6
        arena_model_name: { type: string, example: claude-opus-4-5-20251101 }
        variant: { type: string, example: base }
        harness: { type: string, nullable: true, example: null }
        elo: { type: number, example: 1452.31 }
        prompt_per_m_usd: { type: number, nullable: true, example: 15 }
        completion_per_m_usd: { type: number, nullable: true, example: 75 }
        image_per_unit_usd: { type: number, nullable: true }
        is_or: { type: boolean, example: true }
        wavespeed:
          $ref: "#/components/schemas/WavespeedSummary"

    WavespeedSummary:
      type: object
      nullable: true
      description: |
        Primary Wavespeed variant matching the row's subset, or null when no
        partner-hosted variant matches.
      properties:
        model_id: { type: string }
        url: { type: string, format: uri }
        default_call_usd: { type: number, nullable: true }
        default_call_label: { type: string, nullable: true }
        unit_label: { type: string, nullable: true }

    Placement:
      type: object
      properties:
        category: { $ref: "#/components/schemas/Category" }
        rank: { type: integer }
        elo: { type: number }
        variant: { type: string }
        harness: { type: string, nullable: true }

    LeaderboardResponse:
      type: object
      properties:
        category: { $ref: "#/components/schemas/Category" }
        free: { type: boolean, nullable: true }
        total: { type: integer, example: 360 }
        limit: { type: integer, example: 10 }
        offset: { type: integer, example: 0 }
        models:
          type: array
          items: { $ref: "#/components/schemas/LeaderboardRow" }

    SearchResponse:
      type: object
      properties:
        category: { $ref: "#/components/schemas/Category" }
        filters:
          type: object
          properties:
            free: { type: boolean, nullable: true }
            min_elo: { type: number, nullable: true }
            max_prompt_per_m_usd: { type: number, nullable: true }
            max_completion_per_m_usd: { type: number, nullable: true }
            min_context_length: { type: integer, nullable: true }
            input_modality: { type: string, nullable: true }
            output_modality: { type: string, nullable: true }
        total: { type: integer }
        limit: { type: integer }
        offset: { type: integer }
        models:
          type: array
          items:
            allOf:
              - $ref: "#/components/schemas/LeaderboardRow"
              - type: object
                properties:
                  context_length: { type: integer, nullable: true }
                  input_modalities:
                    type: array
                    items: { type: string }
                  output_modalities:
                    type: array
                    items: { type: string }

    PicksResponse:
      type: object
      properties:
        category: { $ref: "#/components/schemas/Category" }
        top_ranked:
          allOf:
            - $ref: "#/components/schemas/LeaderboardRow"
          nullable: true
        best_value:
          allOf:
            - $ref: "#/components/schemas/LeaderboardRow"
          nullable: true
        recommended:
          allOf:
            - $ref: "#/components/schemas/LeaderboardRow"
          nullable: true

    CompareResponse:
      type: object
      properties:
        requested:
          type: array
          items: { type: string }
        cap: { type: integer, example: 10 }
        truncated: { type: boolean }
        missing:
          type: array
          items: { type: string }
        models:
          type: array
          items:
            type: object
            properties:
              slug: { type: string }
              name: { type: string }
              description: { type: string, nullable: true }
              context_length: { type: integer, nullable: true }
              input_modalities:
                type: array
                items: { type: string }
              output_modalities:
                type: array
                items: { type: string }
              pricing:
                type: object
                additionalProperties: true
              placements:
                type: array
                items: { $ref: "#/components/schemas/Placement" }
              wavespeed_variant_count: { type: integer }

    Provider:
      type: object
      properties:
        name: { type: string }
        tag: { type: string }
        context_length: { type: integer, nullable: true }
        quantization: { type: string, nullable: true }
        status: { type: integer, nullable: true }
        uptime_last_1d: { type: number, nullable: true }
        latency_last_30m: { type: number, nullable: true }
        throughput_last_30m: { type: number, nullable: true }
        pricing:
          type: object
          additionalProperties: true

    WavespeedVariant:
      type: object
      properties:
        model_id: { type: string }
        display_name: { type: string }
        description: { type: string, nullable: true }
        type: { type: string, nullable: true }
        url: { type: string, format: uri }
        default_call_usd: { type: number, nullable: true }
        default_call_label: { type: string, nullable: true }
        unit_label: { type: string, nullable: true }

    ModelDetailResponse:
      type: object
      properties:
        slug: { type: string, example: anthropic/claude-opus-4.6 }
        name: { type: string }
        canonical_slug: { type: string, nullable: true }
        description: { type: string, nullable: true }
        context_length: { type: integer, nullable: true }
        input_modalities:
          type: array
          items: { type: string }
        output_modalities:
          type: array
          items: { type: string }
        pricing:
          type: object
          additionalProperties: true
        leaderboard:
          type: array
          items:
            type: object
            properties:
              subset: { $ref: "#/components/schemas/Category" }
              category: { type: string, example: overall }
              variant: { type: string }
              harness: { type: string, nullable: true }
              rank: { type: integer }
              elo: { type: number }
              vote_count: { type: integer, nullable: true }
        providers:
          type: array
          items: { $ref: "#/components/schemas/Provider" }
        partners:
          type: object
          properties:
            wavespeed:
              type: object
              nullable: true
              properties:
                variants:
                  type: array
                  items: { $ref: "#/components/schemas/WavespeedVariant" }

    HistorySeries:
      type: object
      properties:
        variant: { type: string }
        harness: { type: string, nullable: true }
        points:
          type: array
          items:
            type: object
            properties:
              date: { type: string, format: date, example: "2026-05-22" }
              rank: { type: integer }
              elo: { type: number }
              vote_count: { type: integer, nullable: true }

    HistoryResponse:
      type: object
      properties:
        slug: { type: string }
        category: { $ref: "#/components/schemas/Category" }
        days: { type: integer }
        series:
          type: array
          items: { $ref: "#/components/schemas/HistorySeries" }

    Mover:
      type: object
      properties:
        name: { type: string }
        slug: { type: string }
        rank_was: { type: integer }
        rank_now: { type: integer }
        delta: { type: integer, description: "Positive = climbed (rank went down)." }
        elo: { type: number }

    MoversResponse:
      type: object
      properties:
        category: { $ref: "#/components/schemas/Category" }
        from: { type: string, format: date }
        to: { type: string, format: date }
        days: { type: integer }
        limit: { type: integer }
        climbers:
          type: array
          items: { $ref: "#/components/schemas/Mover" }
        fallers:
          type: array
          items: { $ref: "#/components/schemas/Mover" }

    BestForResponse:
      type: object
      properties:
        slug: { type: string, enum: [coding, multilingual, cheap] }
        title: { type: string }
        description: { type: string }
        total: { type: integer }
        limit: { type: integer }
        models:
          type: array
          items:
            allOf:
              - $ref: "#/components/schemas/LeaderboardRow"
              - type: object
                properties:
                  note: { type: string, nullable: true }

    MetaResponse:
      type: object
      properties:
        categories:
          type: array
          items: { $ref: "#/components/schemas/Category" }
        best_for_slugs:
          type: array
          items: { type: string, enum: [coding, multilingual, cheap] }
        limits:
          type: object
          properties:
            max_limit: { type: integer, example: 500 }
            max_days: { type: integer, example: 365 }
            max_compare_ids: { type: integer, example: 10 }
        endpoints:
          type: array
          items:
            type: object
            properties:
              path: { type: string }
              params:
                type: array
                items: { type: string }

    Error:
      type: object
      properties:
        error: { type: string }
        supported:
          type: array
          items: { type: string }
          description: Present on `unknown_category` / `unknown_slug` errors.

  responses:
    BadRequest:
      description: Bad Request
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    NotFound:
      description: Not Found
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
