openapi: 3.1.0
info:
  title: AI Cost Dashboard — API estática
  version: "1"
  description: >
    API de solo lectura servida como archivos JSON desde el CDN. Cada valor es un Fact con procedencia
    (source_id, source_url, fetched_at, snapshot_sha, method, verified_by, status). Sin autenticación.
    Errores del CDN: 404 estándar; los clientes MCP/CLI los traducen a application/problem+json.
servers:
  - url: /api/v1
paths:
  /catalog.json:
    get:
      summary: Catálogo resumido (listar, filtrar, agrupar)
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/Catalog" } } } }
  /models/{provider}/{slug}.json:
    get:
      summary: Ficha completa de un modelo
      parameters:
        - { name: provider, in: path, required: true, schema: { type: string } }
        - { name: slug, in: path, required: true, schema: { type: string } }
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/ModelFile" } } } }
        "404": { description: Modelo inexistente, content: { application/problem+json: { schema: { $ref: "#/components/schemas/Problem" } } } }
  /providers.json:
    get: { summary: Proveedores, responses: { "200": { description: OK, content: { application/json: { schema: { type: object } } } } } }
  /changes.json:
    get: { summary: Últimos 500 eventos de cambio, responses: { "200": { description: OK, content: { application/json: { schema: { type: object, properties: { changes: { type: array, items: { $ref: "#/components/schemas/ChangeEvent" } } } } } } } } }
  /sources.json:
    get: { summary: Fuentes, términos y estado, responses: { "200": { description: OK, content: { application/json: { schema: { type: object } } } } } }
  /health.json:
    get: { summary: Salud del pipeline, responses: { "200": { description: OK, content: { application/json: { schema: { type: object } } } } } }
components:
  schemas:
    Fact:
      type: object
      required: [value, source_id, fetched_at, status]
      properties:
        value: {}
        unit: { type: string, examples: [usd_per_mtok, usd_per_request] }
        currency: { type: string, enum: [USD, CNY, EUR] }
        source_id: { type: string }
        source_url: { type: string, format: uri }
        fetched_at: { type: string, format: date-time }
        snapshot_sha: { type: string }
        method: { type: string, enum: [api, markdown, html, dataset, llm-extract, decide, override] }
        verified_by: { type: array, items: { type: string } }
        status: { type: string, enum: [verificado, cruzado, fuente-unica, no-verificado, override] }
    ModelSummary:
      type: object
      required: [id, provider_id, name, status]
      properties:
        id: { type: string, examples: [anthropic/claude-sonnet-5-5] }
        provider_id: { type: string }
        name: { $ref: "#/components/schemas/Fact" }
        status: { type: string, enum: [activo, preview, deprecado, archivado] }
        price_input: { oneOf: [{ $ref: "#/components/schemas/Fact" }, { type: "null" }] }
        price_output: { oneOf: [{ $ref: "#/components/schemas/Fact" }, { type: "null" }] }
        blended_3_1: { type: [number, "null"], description: "(3·entrada + salida)/4, USD por millón de tokens" }
        classification: { type: object }
        summary: { type: [string, "null"] }
    Catalog:
      type: object
      required: [generated_at, schema_version, models]
      properties:
        generated_at: { type: string, format: date-time }
        schema_version: { type: string }
        providers: { type: array, items: { type: object } }
        freshness: { type: object }
        workloads: { type: array, items: { type: object } }
        models: { type: array, items: { $ref: "#/components/schemas/ModelSummary" } }
    ModelFile:
      type: object
      properties:
        model: { type: object, description: "Ficha canónica (ver app/packages/core/src/schema/model.ts)" }
        derived: { type: object }
        history: { type: array, items: { $ref: "#/components/schemas/ChangeEvent" } }
        pending_review: { type: array, items: { type: object } }
    ChangeEvent:
      type: object
      properties:
        at: { type: string, format: date-time }
        run_id: { type: string }
        model_id: { type: string }
        field: { type: string }
        old: {}
        new: {}
        decision: { type: string, enum: [auto, review, override, new, archived] }
        rule: { type: string }
    Problem:
      type: object
      required: [type, status, title]
      properties:
        type: { type: string }
        status: { type: integer }
        title: { type: string }
        detail: { type: string }
        instance: { type: string }
