openapi: "3.0.3"
info:
  title: RouterShift API
  description: |
    Enterprise AI model marketplace and multi-protocol inference gateway.

    **Catalog**: Use `/v1/models` or `/api/models`; model availability and prices are runtime data.
    **Pricing**: Dimension-based pricing with API-key price snapshots.
    **Billing**: Idempotent, transaction-safe usage debit, revenue sharing, refunds, and settlement.
    **Multi-tenancy**: Admins, Organizations, Providers, Resellers, and Users each have dedicated consoles.
    **Protocols**: OpenAI, Anthropic Messages, Gemini, MCP tools, and A2A.
    **Versioning**: All responses include an `x-api-version` header with the API version number.
  version: "1.2.0"
  contact:
    name: RouterShift Team

servers:
  - url: https://api.routershift.com
    description: Production

tags:
  - name: Public
    description: No authentication required
  - name: Auth
    description: Login and registration
  - name: Proxy
    description: LLM inference endpoints (API key required)
  - name: Admin
    description: Admin management endpoints (admin key required)
  - name: Console
    description: Reseller/Provider/User self-service (session or admin key)

security:
  - BearerAuth: []
  - ApiKeyHeader: []

paths:
  # ── Public ──
  /health:
    get:
      tags: [Public]
      summary: Health check
      security: []
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: ok

  /readyz:
    get:
      tags: [Public]
      summary: Readiness check
      security: []
      responses:
        "200": {description: Service and dependencies are ready}
        "503": {description: Service is not ready}

  /api/models:
    get:
      tags: [Public]
      summary: List recommended public model products
      description: Returns enabled, public, featured products with runtime metadata and dimension pricing.
      security: []
      responses:
        "200":
          description: Public model list
          content:
            application/json:
              schema:
                type: object
                properties:
                  models:
                    type: array
                    items:
                      $ref: "#/components/schemas/PublicModel"
                  total:
                    type: integer

  /api/models/compare:
    get:
      tags: [Public]
      summary: Compare public models (legacy format)
      security: []
      responses:
        "200":
          description: Model comparison data with merged metadata and pricing

  /api/models/metrics:
    get:
      tags: [Public]
      summary: Public route-transparency metrics
      description: |
        Aggregate quality-of-service (success rate and latency) over a rolling
        30-day window, public list pricing (刊例价), and available-route count for
        featured public (is_featured=true AND is_public=true) models. No provider
        identity, per-provider volume, token, spend, or effective price is exposed.
        List pricing is resolved for a single currency — CNY for the domestic view,
        USD for overseas — so input/output prices never mix currencies.
      parameters:
        - name: currency
          in: query
          required: false
          schema:
            type: string
            enum: [CNY, USD]
            default: USD
          description: Billing currency for list prices (CNY domestic, USD overseas).
      security: []
      responses:
        "200":
          description: Per-model aggregate QoS, list price, and route count
          content:
            application/json:
              schema:
                type: object
                properties:
                  models:
                    type: array
                    items:
                      $ref: "#/components/schemas/PublicModelMetric"
                  window:
                    type: string
                    example: "30d"
                  refreshed_at:
                    type: string
                    format: date-time
                  total:
                    type: integer

  /api/announcements:
    get:
      tags: [Public]
      summary: List published announcements
      security: []
      responses:
        "200":
          description: Announcement list
          content:
            application/json:
              schema:
                type: object
                properties:
                  announcements:
                    type: array
                    items:
                      $ref: "#/components/schemas/Announcement"

  /api/openapi.yaml:
    get:
      tags: [Public]
      summary: OpenAPI specification
      security: []
      responses:
        "200":
          description: OpenAPI 3.0 YAML

  /api/docs:
    get:
      tags: [Public]
      summary: Swagger UI
      security: []
      responses:
        "200":
          description: Swagger UI HTML page

  # ── Auth ──
  /engine/admin/login:
    post:
      tags: [Auth]
      summary: Login with API key
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [api_key]
              properties:
                api_key:
                  type: string
      responses:
        "200":
          description: Login successful
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LoginResponse"

  /engine/admin/register:
    post:
      tags: [Auth]
      summary: Register new user (optionally with invite code)
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                user_email:
                  type: string
                password:
                  type: string
                invite_code:
                  type: string
      responses:
        "200":
          description: Registration successful

  # ── Proxy (API key required) ──
  /v1/version:
    get:
      tags: [Proxy]
      summary: Get data-plane API version
      responses:
        "200": {description: Version information}

  /v1/models:
    get:
      tags: [Proxy]
      summary: List models available to this API key
      description: Availability reflects active products, routes, and key-level access.
      responses:
        "200": {description: OpenAI-compatible model list}

  /v1/chat/completions:
    post:
      tags: [Proxy]
      summary: Chat completions (OpenAI-compatible)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ChatRequest"
      responses:
        "200":
          description: Chat completion
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ChatResponse"
        "402":
          description: Insufficient balance
        "429":
          description: Rate limited

  /v1/chat/completions/estimate:
    post:
      tags: [Proxy]
      summary: Estimate chat usage and price without inference
      responses:
        "200": {description: Usage and price estimate}

  /v1/messages:
    post:
      tags: [Proxy]
      summary: Messages endpoint (Anthropic Messages API compatible)
      responses:
        "200":
          description: Message response

  /v1/embeddings:
    post:
      tags: [Proxy]
      summary: Generate embeddings
      responses:
        "200":
          description: Embedding vector

  /v1/images/generations:
    post:
      tags: [Proxy]
      summary: Generate images
      responses:
        "200":
          description: Generated image

  /v1/images/tasks/{id}:
    get:
      tags: [Proxy]
      summary: Get an asynchronous image task
      description: |
        Reads an existing image task visible to the authenticated caller. Use the
        API key that created it. This endpoint never submits another generation.
        A completed task returns the image result directly; pending and terminal
        non-success states return an id and status.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Completed image result or terminal task status
          headers:
            X-Billing-Status:
              description: Current server-side billing state for this task
              schema:
                type: string
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/ImageGenerationResponse"
                  - $ref: "#/components/schemas/ImageTaskResponse"
        "202":
          description: Image task is still pending
          headers:
            X-Billing-Status:
              description: Current server-side billing state for this task
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ImageTaskResponse"
        "404":
          description: Image task is not visible to this request

  /v1/audio/transcriptions:
    post:
      tags: [Proxy]
      summary: Transcribe audio (multipart/form-data)
      responses:
        "200":
          description: Transcription text

  /v1/audio/speech:
    post:
      tags: [Proxy]
      summary: Text-to-speech
      responses:
        "200":
          description: Audio data

  /v1/tts:
    post:
      tags: [Proxy]
      summary: Text-to-speech compatibility alias
      responses:
        "200": {description: Audio data}

  /v1/transcriptions:
    post:
      tags: [Proxy]
      summary: Speech-to-text compatibility alias
      responses:
        "200": {description: Transcription response}

  /v1/rerank:
    post:
      tags: [Proxy]
      summary: Rerank documents
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [model, query, documents]
              properties:
                model:
                  type: string
                  example: "jina-reranker-v2"
                query:
                  type: string
                documents:
                  type: array
                  items:
                    type: string
                top_n:
                  type: integer
      responses:
        "200":
          description: Reranked results

  /v1/chat/completions/async:
    get:
      tags: [Proxy]
      summary: List asynchronous chat tasks for this API key
      responses:
        "200": {description: Async task list}
    post:
      tags: [Proxy]
      summary: Async chat completions
      description: |
        Submit a chat completion request for async processing.
        Returns a task ID immediately; poll status with GET /v1/chat/completions/async/:id.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ChatRequest"
      responses:
        "202":
          description: Task accepted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AsyncTaskResponse"

  /v1/chat/completions/async/{task_id}:
    get:
      tags: [Proxy]
      summary: Poll async task status
      parameters:
        - name: task_id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Task completed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ChatResponse"
        "202":
          description: Task still processing
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AsyncTaskResponse"

  /v1/chat/completions/async/{task_id}/result:
    get:
      tags: [Proxy]
      summary: Get a completed async task result
      parameters:
        - {name: task_id, in: path, required: true, schema: {type: string}}
      responses:
        "200": {description: Completed chat response}

  /v1/video/generations:
    post:
      tags: [Proxy]
      summary: Create a video generation task
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/VideoGenerationRequest"
      responses:
        "200": {description: Video result or task descriptor}

  /v1/midjourney/imagine:
    post:
      tags: [Proxy]
      summary: Submit a Midjourney-compatible image task
      responses:
        "200": {description: Generation response}

  /v1/suno/generate:
    post:
      tags: [Proxy]
      summary: Submit a Suno-compatible music task
      responses:
        "200": {description: Generation response}

  /v1/video/tasks/{task_id}:
    get:
      tags: [Proxy]
      summary: Get video task status
      parameters:
        - {name: task_id, in: path, required: true, schema: {type: string}}
      responses:
        "200": {description: Video task status}

  /v1/batch:
    get:
      tags: [Proxy]
      summary: List batch tasks for this API key
      responses:
        "200": {description: Batch task list}
    post:
      tags: [Proxy]
      summary: Create a batch task
      responses:
        "202": {description: Batch accepted}

  /v1/batch/{batch_id}:
    get:
      tags: [Proxy]
      summary: Get batch task status
      parameters:
        - {name: batch_id, in: path, required: true, schema: {type: string}}
      responses:
        "200": {description: Batch status}

  /v1/batch/{batch_id}/results:
    get:
      tags: [Proxy]
      summary: Get batch results
      parameters:
        - {name: batch_id, in: path, required: true, schema: {type: string}}
      responses:
        "200": {description: Batch results}

  /v1/batch/{batch_id}/cancel:
    post:
      tags: [Proxy]
      summary: Cancel a batch task
      parameters:
        - {name: batch_id, in: path, required: true, schema: {type: string}}
      responses:
        "200": {description: Batch cancelled}

  /v1/models/gemini/{model}/generateContent:
    post:
      tags: [Proxy]
      summary: Gemini native content generation
      parameters:
        - {name: model, in: path, required: true, schema: {type: string}}
      responses:
        "200": {description: Gemini-compatible response}

  /v1/models/gemini/{model}/streamGenerateContent:
    post:
      tags: [Proxy]
      summary: Gemini native streaming content generation
      parameters:
        - {name: model, in: path, required: true, schema: {type: string}}
      responses:
        "200": {description: Gemini-compatible event stream}

  /v1beta/cachedContents:
    post:
      tags: [Proxy]
      summary: Create an explicitly metered Gemini cache
      description: |
        Requires a verified Google fixed-TTL storage contract. The finite TTL must
        be at most 24 hours. Returns a RouterShift-owned opaque cache name, which
        may be passed as cachedContent to generateContent. Stored tokens and
        contracted TTL are billed separately from subsequent generation usage.
        Unknown upstream outcomes require reconciliation and are not auto-retried.
        Absolute expiry and TTL updates are not supported.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [model, ttl, contents]
              properties:
                model: {type: string}
                ttl: {type: string, example: 3600s}
                contents: {type: array, minItems: 1, items: {type: object}}
                systemInstruction: {type: object}
                tools: {type: array, items: {type: object}}
                toolConfig: {type: object}
                displayName: {type: string}
      responses:
        "200": {description: Owned cache metadata with name and native token usage}
        "400": {description: Invalid or unsupported cache request}
        "402": {description: Insufficient authorized funds}
        "502": {description: Upstream result or usage is unconfirmed; reconciliation required}
        "503": {description: No verified storage route or persistence unavailable}

  /v1beta/cachedContents/{id}:
    parameters:
      - in: path
        name: id
        required: true
        schema: {type: string}
    get:
      tags: [Proxy]
      summary: Read an owned cache's metadata
      responses:
        "200": {description: Live owned cache metadata}
        "404": {description: Cache not owned, expired, deleted, or unavailable}
    delete:
      tags: [Proxy]
      summary: Delete an owned cache
      description: Idempotent deletion does not refund the fixed contracted TTL. Native provider resource identifiers cannot be supplied in place of RouterShift cache identifiers.
      responses:
        "200": {description: Deletion confirmed; empty JSON object}
        "404": {description: Cache not owned or unavailable}
        "502": {description: Upstream deletion unconfirmed}

  /v1beta/models/{model}:generateContent:
    post:
      tags: [Proxy]
      summary: Gemini SDK-compatible content generation
      parameters:
        - {name: model, in: path, required: true, schema: {type: string}}
      responses:
        "200": {description: Gemini-compatible response}

  /v1beta/models/{model}:streamGenerateContent:
    post:
      tags: [Proxy]
      summary: Gemini SDK-compatible streaming content generation
      parameters:
        - {name: model, in: path, required: true, schema: {type: string}}
      responses:
        "200": {description: Gemini-compatible event stream}

  /v1/mcp/:
    post:
      tags: [Proxy]
      summary: MCP JSON-RPC tools gateway
      description: Supports initialize, tools/list, and tools/call. Resources and prompts are not currently exposed.
      responses:
        "200": {description: JSON-RPC response}

  /v1/mcp/sse:
    get:
      tags: [Proxy]
      summary: Open an MCP server-sent event stream
      responses:
        "200": {description: MCP event stream}

  /v1/a2a/:
    post:
      tags: [Proxy]
      summary: A2A JSON-RPC gateway
      description: Supports message/send, message/stream, tasks/get, and tasks/cancel.
      responses:
        "200": {description: JSON-RPC or streaming response}

  /.well-known/agent.json:
    get:
      tags: [Public]
      summary: Get the public A2A Agent Card
      security: []
      responses:
        "200": {description: A2A Agent Card}

  /v1/a2a/sse:
    get:
      tags: [Proxy]
      summary: Open an A2A server-sent event stream
      responses:
        "200": {description: A2A event stream}

  # ── Console User ──
  /console/user/keys:
    get:
      tags: [Console]
      summary: List user API keys
      security: [{BearerAuth: []}]
      responses:
        "200": {description: "List of API keys"}
    post:
      tags: [Console]
      summary: Create API key
      security: [{BearerAuth: []}]
      requestBody:
        content: {application/json: {schema: {type: object, properties: {name: {type: string}, models: {type: array, items: {type: string}}}, required: [name]}}}
      responses:
        "200": {description: "Created key with full key shown once"}

  /console/user/wallet:
    get:
      tags: [Console]
      summary: Get wallet balance
      security: [{BearerAuth: []}]
      responses:
        "200": {description: "Wallet balance and frozen balance"}

  /console/user/budget-status:
    get:
      tags: [Console]
      summary: Get current budget usage vs limits
      security: [{BearerAuth: []}]
      responses:
        "200":
          description: Dual-lane budget status (USD + CNY spend vs cap)
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      max_budget_usd:
                        type: number
                      max_budget_cny:
                        type: number
                      spend_usd:
                        type: number
                      spend_cny:
                        type: number
                      pct_usd:
                        type: number
                      pct_cny:
                        type: number
                      pct:
                        type: number
                      level:
                        type: string
                        enum: [normal, warning, critical, fatal]

  /console/user/spend:
    get:
      tags: [Console]
      summary: List spend logs
      security: [{BearerAuth: []}]
      parameters:
        - {name: limit, in: query, schema: {type: integer, default: 50}}
        - {name: offset, in: query, schema: {type: integer, default: 0}}
        - {name: model, in: query, schema: {type: string}}
      responses:
        "200": {description: "Paginated spend logs"}

  /console/user/bills:
    get:
      tags: [Console]
      summary: List cost bills
      security: [{BearerAuth: []}]
      parameters:
        - {name: limit, in: query, schema: {type: integer, default: 50}}
      responses:
        "200": {description: "Paginated bills"}

  /console/user/invoices:
    get:
      tags: [Console]
      summary: List invoices
      security: [{BearerAuth: []}]
      responses:
        "200": {description: "List of invoices"}

  /console/user/profile:
    get:
      tags: [Console]
      summary: Get user profile
      security: [{BearerAuth: []}]
      responses:
        "200": {description: "User profile"}
    post:
      tags: [Console]
      summary: Update user profile
      security: [{BearerAuth: []}]
      requestBody:
        content: {application/json: {schema: {type: object, properties: {user_alias: {type: string}, user_email: {type: string}, old_password: {type: string}, new_password: {type: string}}}}}
      responses:
        "200": {description: "Profile updated"}

  /console/user/export:
    post:
      tags: [Console]
      summary: Export user data (GDPR)
      security: [{BearerAuth: []}]
      responses:
        "200": {description: "JSON export of all user data"}

  /console/user/delete-account:
    post:
      tags: [Console]
      summary: Self-service account deletion
      security: [{BearerAuth: []}]
      requestBody:
        content: {application/json: {schema: {type: object, properties: {confirm: {type: string}}, required: [confirm]}}}
      responses:
        "200": {description: "Account soft-deleted"}

  /console/user/webhooks:
    get:
      tags: [Console]
      summary: List user webhooks
      security: [{BearerAuth: []}]
      responses:
        "200": {description: "List of webhooks"}
    post:
      tags: [Console]
      summary: Create webhook
      security: [{BearerAuth: []}]
      requestBody:
        content: {application/json: {schema: {type: object, properties: {url: {type: string}, events: {type: array, items: {type: string}}, description: {type: string}}, required: [url, events]}}}
      responses:
        "200": {description: "Webhook created"}

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      description: "API key (sk-* prefix) or session token (rs-* prefix)"
    ApiKeyHeader:
      type: apiKey
      in: header
      name: x-api-key
      description: "Data-plane API key alternative to Authorization: Bearer"

  schemas:
    PublicModel:
      type: object
      properties:
        model_code:
          type: string
          example: "qwen3.7-plus"
        provider_name:
          type: string
          example: "Alibaba"
        display_name:
          type: string
          example: "Qwen 3.7 Plus"
        description:
          type: string
        total_context:
          type: integer
        max_output_tokens:
          type: integer
        capabilities:
          type: array
          items:
            type: string
          example: ["text", "function_calling"]
        pricing:
          type: object
          additionalProperties:
            oneOf:
              - type: number
              - type: string
              - type: "null"
        is_featured:
          type: boolean
        is_official:
          type: boolean
        category:
          type: string
          example: "chat"

    PublicModelMetric:
      type: object
      properties:
        model_code:
          type: string
        success_rate:
          type: number
          nullable: true
        latency_p50_ms:
          type: number
          nullable: true
        latency_p95_ms:
          type: number
          nullable: true
        latency_p99_ms:
          type: number
          nullable: true
        sufficient_samples:
          type: boolean
          description: false when request volume is below the QoS sample threshold
        list_price_currency:
          type: string
          example: "CNY"
        list_price_input:
          type: number
        list_price_output:
          type: number
        route_count:
          type: integer

    Announcement:
      type: object
      properties:
        id:
          type: integer
        title:
          type: string
        content:
          type: string
        severity:
          type: string
        is_published:
          type: boolean
        created_at:
          type: string

    LoginResponse:
      type: object
      properties:
        token:
          type: string
        role:
          type: string
        user_id:
          type: string

    ChatRequest:
      type: object
      required: [model, messages]
      properties:
        model:
          type: string
          example: "gpt-4"
        messages:
          type: array
          items:
            type: object
            properties:
              role:
                type: string
                enum: [system, user, assistant, tool]
              content:
                oneOf:
                  - type: string
                  - type: array
        max_tokens:
          type: integer
        temperature:
          type: number
        top_p:
          type: number
        stream:
          type: boolean
        tools:
          type: array
        tool_choice:
          type: string
        cache:
          type: object
          description: Explicit response-cache policy. Semantic reuse is limited to eligible deterministic, non-streaming text requests and isolated per API key.
          properties:
            mode:
              type: string
              enum: [semantic]
            max_age:
              type: integer
              minimum: 1
              description: Maximum acceptable cache age in seconds.
            force_refresh:
              type: boolean
              default: false
            namespace:
              type: string
              description: Application-defined namespace, scoped again by the authenticated API key.

    ChatResponse:
      type: object
      properties:
        id:
          type: string
        object:
          type: string
        created:
          type: integer
        model:
          type: string
        choices:
          type: array
          items:
            type: object
            properties:
              index:
                type: integer
              message:
                $ref: "#/components/schemas/Message"
              finish_reason:
                type: string
        usage:
          $ref: "#/components/schemas/Usage"

    Message:
      type: object
      properties:
        role:
          type: string
        content:
          type: string

    Usage:
      type: object
      properties:
        prompt_tokens:
          type: integer
        completion_tokens:
          type: integer
        total_tokens:
          type: integer

    AsyncTaskResponse:
      type: object
      properties:
        id:
          type: string
          description: Task ID for polling
        status:
          type: string
          enum: [pending, running, completed, failed]
        created_at:
          type: string
          format: date-time
        error:
          type: string
          description: Error message if status is failed

    EmbeddingRequest:
      type: object
      required: [model, input]
      properties:
        model:
          type: string
          example: "text-embedding-3-small"
        input:
          oneOf:
            - type: string
              example: "The quick brown fox jumped over the lazy dog."
            - type: array
              items:
                type: string
        encoding_format:
          type: string
          enum: [float, base64]
          default: float
        dimensions:
          type: integer
          description: Number of dimensions (model-dependent)

    ImageGenerationRequest:
      type: object
      required: [model, prompt]
      properties:
        model:
          type: string
          example: "dall-e-3"
        prompt:
          type: string
          example: "A serene mountain landscape at sunset"
        n:
          type: integer
          default: 1
        size:
          type: string
          enum: ["1024x1024", "1792x1024", "1024x1792", "512x512", "256x256"]
          default: "1024x1024"
        quality:
          type: string
          enum: [standard, hd]
          default: standard
        style:
          type: string
          enum: [vivid, natural]
          default: vivid

    VideoGenerationRequest:
      type: object
      required: [model]
      properties:
        model: {type: string}
        prompt: {type: string}
        duration: {type: integer, description: "Requested seconds; route-specific bounds apply."}
        size: {type: string}
        fps: {type: integer}
        n: {type: integer, enum: [1]}
        negative_prompt: {type: string}
        image_url: {type: string, format: uri}
        media:
          type: array
          items:
            type: object
            required: [type, url]
            properties:
              type: {type: string}
              url: {type: string, format: uri}
        ratio: {type: string}
        generate_audio:
          type: boolean
          description: "Ark Seedance only. Omit to preserve the provider default; false is transmitted explicitly."

    ImageGenerationResponse:
      type: object
      required: [data]
      properties:
        created:
          type: integer
          description: Optional Unix timestamp; native normalized results may omit it.
        request_id:
          type: string
        data:
          type: array
          items:
            type: object
            properties:
              url:
                type: string
                format: uri
              b64_json:
                type: string
              revised_prompt:
                type: string
              size:
                type: string
        usage:
          type: object
          additionalProperties: true

    ImageTaskResponse:
      type: object
      required: [id, status]
      properties:
        id:
          type: string
        status:
          type: string
          description: pending, released, reconciliation_required, or another server-returned terminal state
        status_url:
          type: string
        error:
          type: string
