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.
    **Client parity**: REST, the first-party Python/Node SDKs, and the local model-execution MCP tools use the same key permissions, budgets, model operations, and billing. Client schema support does not certify every model or execution mode.
    **Paid submissions**: A request ID is for correlation, not a public idempotency guarantee. Do not blindly replay image/video generation after a timeout, lost response, or task lookup error.
    **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: Lists explicitly granted request model names with an enabled, execution-eligible catalog route and an active sell price in the API key's billing currency. Discovery does not guarantee balance, budget, current rate-limit admission, or support for every modality.
      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/messages/count_tokens:
    post:
      tags: [Proxy]
      summary: Count input tokens for an Anthropic Messages request
      description: |
        Anthropic SDK-compatible preflight endpoint. For requests containing
        only text, thinking and tools, RouterShift returns an explainable UTF-8
        byte upper bound and identifies that method in both the response body
        and `X-RouterShift-Token-Count-Method` response header. The additional
        `count_method`, `exact` and `scope` properties are backward-compatible
        RouterShift extensions; clients may continue reading only
        `input_tokens`. Image and document inputs are rejected because their
        token cost depends on provider-native preprocessing and cannot be
        inferred safely from payload bytes. This endpoint does not invoke a
        generation route, reserve funds, or create billable usage.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AnthropicMessagesCountTokensRequest"
      responses:
        "200":
          description: Conservative textual input-token upper bound
          headers:
            X-RouterShift-Token-Count-Method:
              schema:
                type: string
                enum: [utf8_byte_upper_bound]
          content:
            application/json:
              schema:
                type: object
                required: [input_tokens, count_method, exact, scope]
                properties:
                  input_tokens:
                    type: integer
                    minimum: 1
                  count_method:
                    type: string
                    enum: [utf8_byte_upper_bound]
                    description: Method used to derive input_tokens; never provider billing usage.
                  exact:
                    type: boolean
                    enum: [false]
                    description: Always false because RouterShift does not claim tokenizer-exact counts.
                  scope:
                    type: string
                    enum: [text_thinking_tools]
                    description: Content classes covered by this estimate.
        "400":
          description: Invalid, unsupported, or provider-native media semantics
        "403":
          description: Model is not available to this API key

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

  /v1/images/generations:
    post:
      tags: [Proxy]
      summary: Generate images
      description: |
        Image options depend on the selected model, route, and accepted execution mode.
        A model listed by /v1/models is not proof that this operation is enabled.
        Generation is not an idempotent POST: on a timeout or lost response, do not
        automatically submit it again. The normal data-plane request-body limit is
        2 MiB, including all JSON-encoded references and Base64 data.
        Use the RouterShift model code permitted by your key and sufficient wallet
        or approved credit and key budget. An HTTP success or an available result
        alone does not prove financial settlement. Missing or inconsistent usage
        can require reconciliation; stopping the client does not cancel the work.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ImageGenerationRequest"
      responses:
        "200":
          description: Generated image
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ImageGenerationResponse"
        "202":
          description: Accepted asynchronous image task; retain its id for lookup
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ImageTaskResponse"
        "400":
          description: Invalid request or explicit upstream rejection; retain X-Request-ID and do not invent a task id
          headers:
            X-Request-ID:
              description: RouterShift correlation ID, not an idempotency key
              schema: {type: string}
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProxyErrorResponse"
        "413":
          description: Request body exceeds the data-plane limit
        "422":
          description: Selected operation or execution mode is not verified; no upstream call is made for this refusal
        "502":
          description: Upstream or response verification failure; the outcome may require reconciliation, not automatic resubmission
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProxyErrorResponse"

  /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.
        A reconciliation_required status is not a free failure or permission to
        repeat the POST. Retain the original task ID and request ID for support.
        A synchronous result without a task ID cannot be retrieved here.
      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)
      description: |
        Uses the exact selected product's reviewed transcription contract.
        The local gpt-transcribe candidate requires an explicit native OpenAI
        connection, one model field and one canonical mono PCM16 WAV file at
        16 kHz, at most 60 seconds; only non-streaming JSON is admitted. Other
        optional fields are not enabled by this narrow candidate.
        Billable duration comes from native usage.type=duration and usage.seconds,
        never WAV geometry or subtitle timing. Missing or conflicting usage keeps
        the consumed outcome in reconciliation; do not resubmit to recover proof.
        Successful empty transcription can still incur a charge. This contract
        does not enable an unaccepted product or guarantee automatic recovery of
        a receipt that was not persisted before process failure.
      responses:
        "200":
          description: Transcription text

  /v1/audio/speech:
    post:
      tags: [Proxy]
      summary: Text-to-speech
      description: |
        Uses the selected product's verified audio operation and frozen tariff.
        Binary output is the default. Accepted Seed Audio routes may opt into
        response_mode=json for bounded native subtitle metadata; playback duration
        is not billable duration. Unreviewed Seed speaker assets are rejected before
        dispatch. The pinned Bailian native character route requires JSON mode and
        returns an expiring audio URL, not binary audio or Seed metadata.
        Do not retry an unknown consumed outcome to retrieve missing results.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [model, input]
              properties:
                model: {type: string}
                input: {type: string}
                voice: {type: string, description: Only voices accepted by the exact native operation.}
                response_format: {type: string}
                response_mode:
                  type: string
                  enum: [binary, json]
                  default: binary
                  description: Platform presentation option; JSON is route-specific and does not enable unsupported billing.
                references:
                  type: array
                  maxItems: 3
                  items: {type: object}
                  description: Seed media references; unreviewed speaker assets are not accepted.
                audio_config:
                  type: object
                  properties:
                    enable_subtitle: {type: boolean, default: false}
      responses:
        "200":
          description: Binary audio or the explicitly requested route-specific JSON result; inspect X-Billing-Status independently.
          headers:
            X-Request-ID: {schema: {type: string}}
            X-Billing-Status:
              schema: {type: string, enum: [billed, waived, reconciliation_required, pending]}
          content:
            application/octet-stream:
              schema: {type: string, format: binary}
            audio/mpeg:
              schema: {type: string, format: binary}
            audio/wav:
              schema: {type: string, format: binary}
            audio/ogg:
              schema: {type: string, format: binary}
            application/json:
              schema:
                oneOf:
                  - type: object
                    required: [audio, content_type, subtitles_omitted, timestamp_unit, timing_basis]
                    properties:
                      audio: {type: string, format: byte}
                      content_type: {type: string}
                      playback_duration: {type: number, description: Optional native playback seconds; never billing quantity.}
                      subtitles_omitted: {type: boolean}
                      timestamp_unit: {type: string, enum: [milliseconds]}
                      timing_basis: {type: string, enum: [native_audio]}
                      subtitle:
                        type: object
                        properties:
                          text: {type: string}
                          sentences:
                            type: array
                            maxItems: 256
                            items:
                              type: object
                              required: [start_time, end_time, text]
                              properties:
                                start_time: {type: integer, minimum: 0}
                                end_time: {type: integer, minimum: 0}
                                text: {type: string}
                                words:
                                  type: array
                                  description: At most 2048 words across the response; native millisecond offsets.
                                  items:
                                    type: object
                                    required: [start_time, end_time, text]
                                    properties:
                                      start_time: {type: integer, minimum: 0}
                                      end_time: {type: integer, minimum: 0}
                                      text: {type: string}
                  - type: object
                    required: [audio_url, audio_id, expires_at, content_type, request_id]
                    properties:
                      audio_url: {type: string, format: uri}
                      audio_id: {type: string}
                      expires_at: {type: integer, description: Native audio URL expiration; no extended lifetime is promised.}
                      content_type: {type: string, enum: [audio/wav]}
                      request_id: {type: string, description: Native request identity; X-Request-ID is the platform identity.}
        "400": {description: Invalid route-specific request; no upstream invocation.}
        "422": {description: The exact product operation or metadata mode is not verified; no upstream invocation.}
        "502": {description: Missing or conflicting native consumption evidence; retain the original request ID and do not automatically resubmit.}

  /v1/tts:
    post:
      tags: [Proxy]
      summary: Text-to-speech compatibility alias
      description: Same operation, presentation and billing boundaries as /v1/audio/speech; not a fallback or retry endpoint.
      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 with read-only execution and billing diagnostics
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/AsyncTaskResponse"
    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
      description: |
        Media roles and parameters are model/route-specific. On an explicit
        upstream rejection there is no accepted task ID to query. A lost response
        is an uncertain outcome, not proof of rejection or permission to retry a
        potentially billable POST. Retain the safe error and X-Request-ID.
        Use your key's RouterShift model code. Store a non-empty returned task_id
        before waiting; query it with the same key. A 2xx failed descriptor can
        contain an empty task_id and require reconciliation; do not invent one.
        Requested duration is not final
        billable usage, and a completed task does not itself prove settlement.
        Missing or inconsistent final usage requires reconciliation, not guessed
        quantities, a new submission, or an automatic refund.
        Keep business-job, submission, lookup, delivery and billing outcomes
        separate. X-Request-ID is correlation, not a replay guarantee. Clients
        must not switch to SDK/MCP to repeat an uncertain submission. Delivery
        recovery uses an existing task/result only; it does not create a video.
        Reservation estimates and key budgets do not establish a provider-enforced
        per-request usage or cost ceiling. Native Seedance settles completion_tokens,
        not estimated duration/frame quantities. A caller requiring a strict cost
        ceiling must withhold new submissions without verified service-specific
        upper bounds; querying an existing task is not a new submission.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/VideoGenerationRequest"
      responses:
        "200":
          $ref: "#/components/responses/VideoSubmission"
        "201":
          $ref: "#/components/responses/VideoSubmission"
        "202":
          $ref: "#/components/responses/VideoSubmission"
        "400":
          description: Invalid request or explicit upstream rejection; no task ID is promised
          headers:
            X-Request-ID:
              description: RouterShift correlation ID, not an idempotency key
              schema: {type: string}
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProxyErrorResponse"
        "413": {description: Request body exceeds the 2 MiB data-plane limit}
        "422": {description: Selected product is not verified for this execution mode; no upstream call is made for this refusal}
        "502":
          description: Upstream response or submission outcome is unavailable; do not automatically repeat the POST
          headers:
            X-Request-ID:
              description: RouterShift correlation ID, not an idempotency key
              schema: {type: string}
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProxyErrorResponse"

  /v1/midjourney/imagine:
    post:
      tags: [Proxy]
      summary: Registered legacy Midjourney-compatible image task endpoint
      description: Not commercially available. Native metering and accepted/unknown task lifecycle have not passed production acceptance; route registration does not certify a sellable capability.
      responses:
        "200": {description: Generation response}

  /v1/suno/generate:
    post:
      tags: [Proxy]
      summary: Registered legacy Suno-compatible music task endpoint
      description: Not commercially available. Native metering and accepted/unknown task lifecycle have not passed production acceptance; route registration does not certify a sellable capability.
      responses:
        "200": {description: Generation response}

  /v1/video/tasks/{task_id}:
    get:
      tags: [Proxy]
      summary: Get video task status
      description: |
        Query the task_id returned by video generation with the same API key.
        This GET never starts a new generation. Native Ark Seedance and Wan
        adapters return fixed public explanations for failed tasks. Other
        compatible routes can preserve provider response fields; treat their
        error text as untrusted and review it before sharing or logging.
        A query error, timeout or 404/410 does not prove that the original task
        consumed nothing, was cancelled or has been refunded. Do not re-create it
        automatically. Keep the original task ID and submission X-Request-ID.
        Terminal states such as released and reconciliation_required stop local
        polling but do not establish generation success, zero charge or refund.
      parameters:
        - {name: task_id, in: path, required: true, schema: {type: string}}
      responses:
        "200":
          description: Video task status; completion and financial settlement are separate facts
          headers:
            X-Request-ID:
              description: Identifies this lookup; not a substitute for original submission evidence. Clients may reuse the same correlation value.
              schema: {type: string}
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VideoTaskResponse"
        "404":
          description: Task not visible to this key, or upstream task status unavailable; not permission to resubmit
        "410": {description: Upstream task status no longer available; retain original evidence and contact support}
        "502":
          description: Task status could not be retrieved; do not submit another generation
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProxyErrorResponse"
        default:
          description: Other platform or upstream HTTP errors, including throttling and server failures. Preserve the original task ID; a lookup failure is not a generation failure or permission to resubmit.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProxyErrorResponse"

  /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: |
        External-tool gateway for tools registered by an API key. Supports
        initialize, tools/list, and tools/call. Resources and prompts are not
        currently exposed. This is not the local routershift-mcp model-execution
        server shipped in the Node SDK: its eight tools use local stdio and call
        the ordinary REST model endpoints. See the website SDK/MCP setup guide.
      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:
  responses:
    VideoSubmission:
      description: Video result or task descriptor; check status and a non-empty task_id as well as HTTP status
      headers:
        X-Request-ID:
          description: RouterShift submission correlation ID, not an idempotency key
          schema: {type: string}
        X-Billing-Status:
          description: When supplied as reconciliation_required, retain the original evidence; it is not proof of a refund or permission to repeat the POST. Absence does not prove settlement.
          schema: {type: string, enum: [reconciliation_required]}
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/VideoTaskResponse"
  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.

    AnthropicMessagesCountTokensRequest:
      type: object
      required: [model, messages]
      additionalProperties: true
      properties:
        model:
          type: string
        messages:
          type: array
          items:
            type: object
            required: [role, content]
            additionalProperties: true
            properties:
              role:
                type: string
                enum: [user, assistant]
              content:
                oneOf:
                  - type: string
                  - type: array
                    items:
                      type: object
                      additionalProperties: true
                      description: Anthropic content block. Media and unsupported nested tool-result blocks receive HTTP 400 from this estimator.
        system:
          oneOf:
            - type: string
            - type: array
              items:
                type: object
                additionalProperties: true
        tools:
          type: array
          items:
            type: object
            required: [name, input_schema]
            additionalProperties: true
            properties:
              name:
                type: string
              description:
                type: string
              input_schema:
                type: object
                additionalProperties: true
        tool_choice:
          oneOf:
            - type: string
            - type: object
              additionalProperties: true
        thinking:
          type: object
          additionalProperties: true
          properties:
            type:
              type: string
              enum: [enabled, disabled]
            budget_tokens:
              type: integer
        metadata:
          type: object
          additionalProperties: true

    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: [queued, processing, completed, failed]
          description: Execution state; completed means the result is available, not that billing settled.
        execution_status:
          type: string
          enum: [queued, processing, completed, failed]
          description: Read-only execution state on task status and list responses.
        billing_status:
          type: string
          enum: [pending, upstream_consumed, settlement_pending, reconciliation_required, released, settled, billed, waived, unknown]
          description: Persisted billing fact or operational projection; unknown means no matching fact was established. No charge or refund is inferred from execution success.
        reason_code:
          type: string
          enum: [execution_pending, execution_failed, billing_pending, billing_reconciliation_required, reservation_released, billing_status_unavailable, result_available]
        next_action:
          type: string
          enum: [query_existing_task, use_result_and_query_billing_status, inspect_existing_task_do_not_resubmit, use_result]
        retry_allowed:
          type: boolean
          enum: [false]
          description: A status read never grants authority to repeat the existing operation.
        created:
          type: integer
          description: Task acceptance Unix timestamp in seconds.
        completed:
          type: integer
          description: Terminal execution Unix timestamp in seconds, when available; not settlement time.
        created_at:
          type: string
          format: date-time
          deprecated: true
          description: Legacy optional documentation field; current task responses use created Unix seconds.
        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]
      description: "Fields describe the client access surface, not universal model capability. Confirm the deployed gateway version and the exact route's accepted operation and execution mode. Advanced media operations under offline development are not enabled by this schema. Generation POSTs are not automatically replayable after an uncertain outcome."
      properties:
        model:
          type: string
          description: RouterShift model code visible to this API key, not an upstream model ID.
        prompt:
          type: string
          description: Text prompt; required for many model-specific generation modes.
        image:
          description: Reference image or images, where supported by the selected model and route. Omit for text-to-image; do not send null or an empty collection to request editing. Input format and count are model-specific; references are passed to the upstream rather than uploaded or hosted by RouterShift. Use accessible media and obtain the required rights/consent; success from an image model does not establish eligibility for a video model.
          oneOf:
            - type: string
            - type: array
              items:
                type: string
        async:
          type: boolean
          description: Request an asynchronous image task only for a separately accepted route and upstream execution mode; model visibility alone does not enable it.
        n:
          type: integer
          description: Requested maximum output count; supported values are model-specific.
        size:
          type: string
          description: Model-specific output size, such as a supported resolution tier or widthxheight pixel value. No gateway-wide size enum or default is imposed.
        negative_prompt:
          type: string
          description: Model-specific exclusion prompt; omit unless supported by the selected route.
        quality:
          type: string
          description: Model-specific quality option; omit unless accepted for the selected route.
        style:
          type: string
          description: Model-specific style option; omit unless accepted for the selected route.
        seed:
          type: integer
          minimum: 0
          maximum: 2147483647
          description: Optional native Qwen Image 3.0/Pro seed. Explicit zero is preserved; omission retains the provider default. Other routes require their own verified contract.
        watermark:
          type: boolean
          description: Optional native Qwen watermark control. Explicit false is preserved; omission retains the provider default. Support on other image routes is model-specific.
        prompt_extend:
          type: boolean
          description: Optional native Qwen prompt extension switch. Explicit false is preserved; omission retains the provider default.
        enable_thinking:
          type: boolean
          description: Optional native Qwen prompt thinking switch, applied by the provider only when prompt extension is enabled. Does not implicitly enable prompt_extend; explicit false and omission remain distinct.
        prompt_extend_mode:
          type: string
          enum: [direct, agent]
          description: Optional native Qwen prompt extension mode. Agent mode supports text-to-image only and is rejected with reference images. Omission retains the provider default.
        response_format:
          type: string
          enum: [url, b64_json]
          description: Requested output representation, where supported by the selected route. The current native Qwen Image 3.0 route returns URLs and does not honor b64_json.

    VideoGenerationRequest:
      type: object
      required: [model]
      description: "Options are validated against the selected model and route. SDK/MCP support does not establish route acceptance or deployment. Advanced edit, extension and specialist video operations remain offline development, not enabled runtime capabilities. Preserve the original task ID and do not automatically retry an uncertain generation POST."
      properties:
        model: {type: string, description: "RouterShift model code, not the upstream provider model ID."}
        prompt: {type: string}
        duration: {type: integer, description: "Requested seconds; route-specific bounds apply."}
        size: {type: string, description: "Model-specific resolution; accepted Ark Seedance and Wan 3.0 routes support 480p, 720p and 1080p."}
        fps: {type: integer}
        n: {type: integer, enum: [1]}
        negative_prompt: {type: string}
        image_url: {type: string, format: uri, description: "Convenience input mapped to first_frame on native Ark Seedance and Wan 3.0 routes. Do not also provide a media first_frame."}
        media:
          type: array
          items:
            type: object
            required: [type, url]
            properties:
              type: {type: string, description: "Route-specific role such as first_frame, last_frame or reference_image. Do not duplicate frame roles. Some routes require a first_frame whenever last_frame is supplied; do not assume frame and reference modes can be combined."}
              url: {type: string, format: uri}
        ratio: {type: string, description: "Route-specific aspect ratio; Ark Seedance first-frame mode uses adaptive."}
        seed:
          type: integer
          minimum: -1
          maximum: 2147483647
          description: "Optional native Wan 3.0 seed: -1 or 0 through 2147483647. Explicit zero is preserved; omission retains the provider default. The current Ark Seedance contract rejects an explicit seed."
        watermark:
          type: boolean
          description: "Optional native Wan 3.0 and Ark Seedance watermark control. Explicit false is preserved; omission retains the provider default. Other routes require their own verified contract."
        prompt_extend:
          type: boolean
          description: "Optional native Wan 3.0 prompt extension switch. Explicit false is preserved, but file/link media requires true or omission. The current Ark Seedance contract rejects an explicit prompt_extend."
        generate_audio:
          type: boolean
          description: "Optional audio control on verified native Ark Seedance and Wan 3.0 routes only. Ark uses generate_audio; Wan maps this field to parameters.audio. Omit to preserve the provider default (Wan currently enables audio); false explicitly requests no audio track. Wan audio on/off uses the same video tariff, not an extra audio meter. Other routes reject an explicit value; SDK/MCP field availability does not prove route support."

    ProxyErrorResponse:
      type: object
      required: [error]
      properties:
        request_id:
          type: string
          description: Optional RouterShift correlation ID on platform-generated errors. Always retain X-Request-ID when present; neither field grants safe POST replay.
        error:
          type: object
          required: [message, type, code]
          properties:
            message: {type: string, description: "Public diagnostic text, not a stable classification. Native Qwen, Ark Seedance and Wan adapters use fixed messages for recognized failures; other compatible routes can retain provider text. Review before sharing or logging; use code and X-Request-ID for correlation."}
            type: {type: string}
            code: {type: string, description: "Stable public error code; UPSTREAM_ERROR alone does not identify the native cause."}

    VideoTaskResponse:
      type: object
      required: [task_id, status]
      properties:
        task_id:
          type: string
          description: Store a non-empty value returned by the platform. An accepted failed submission can contain an empty string; do not fabricate an ID or query it as a task.
        status:
          type: string
          description: Model-dependent task state such as pending, queued, running, processing, completed, failed, cancelled, expired, released or reconciliation_required. Do not treat an unknown state as success. Terminal financial states do not imply a successful video or refund.
        video_url:
          type: string
          format: uri
        thumbnail:
          type: string
          format: uri
        error:
          type: string
          description: Explanation for a terminal non-success state, when supplied. Native Ark Seedance and Wan adapters use fixed public messages; compatible routes can preserve provider text. Do not log it blindly. A task error alone does not establish a zero charge.
        usage:
          type: object
          additionalProperties: true
          description: Optional model-specific usage returned by task lookup. Use platform billing records for charges; do not calculate bills from requested duration or guess missing usage.

    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
