0G Labs Provider API

The Provider API from 0G Labs — 1 operation(s) for provider.

OpenAPI Specification

0g-labs-provider-api-openapi.yml Raw ↑
openapi: 3.2.0
info:
  description: '0G Router is an API gateway between users and the decentralized 0G Compute Provider network.

    It provides unified access, fee collection, and intelligent routing for AI inference services.


    ## Balance model: Router ledger vs Payment Layer vault


    User funds live in the shared 0G Payment Layer (PL) vault — a multi-app funding pool that other 0G products also draw from. Router does not drain a user''s full vault balance up front; it pulls small amounts on demand into its own ledger as inference is consumed.


    As a result, the `/v1/account/balance` endpoint returns the **Router ledger only** (`deposit_balance + credit_balance`). It deliberately excludes the PL vault, because vault balance is shared across consumer apps and is not yet committed to Router.


    ### Picking an endpoint: `/balance` vs `/funds`


    If you just want one **display** balance that already combines both sides, call `/v1/account/funds`. Router does the aggregation server-side and returns a net `total` = `(deposit + credit − pending_charge) + vault_balance`, plus the breakdown. Unlike `/balance`''s `total_balance` (Router-ledger-only, always `≥ 0`), `/funds.total` includes the full vault and **may be negative** when the user owes Router (`pending_charge` exceeds available funds) — render that as "owed to Router".


    Use `/v1/account/balance` (not `/funds`) for settlement / SDK / mgmt-key integrations that depend on the Router-ledger-only, `≥ 0` figure. And note `/funds.total` is a wallet-**display** number: it is **not** the admission figure — the "can I submit a request" calculation below applies `vault_ratio` and is computed separately.


    ### Admission rule


    An inference request is admitted whenever either side has enough funds:


    - Router ledger covers `min_cost`, **or**

    - `vault_balance × vault_ratio − pending_charge >= min_cost` (deferred path; the shortfall is recorded as `pending_charge` and cleared from the next PL pull).


    Otherwise the request returns `402 insufficient_balance`.


    ### Auto-pull from the vault (mainnet values)


    After a user''s **first successful inference charge**, the account becomes pull-eligible. A background worker then keeps the Router ledger topped up from the PL vault:


    | Parameter | Mainnet value | Meaning |

    |---|---|---|

    | Scan interval | 3 s | How often Router checks each pull-eligible account |

    | Low watermark | 0.1 0G | Pull triggers when effective Router balance drops below this |

    | High watermark | 0.5 0G | Target balance after a pull (≈ amount pulled per cycle) |

    | Vault ratio | 0.5 | Only 50% of the PL vault counts toward admission (safety margin for the shared pool) |

    | min_cost | 0.01 0G | Minimum cost a single request must be able to cover |


    Accounts that have only deposited to the vault but never sent an inference request will not trigger a pull.


    ### Predicting "can submit" on the client


    To gate a submit button before the first request, combine both sides:


    ```

    available = router.total_balance + max(0, vault_balance × vault_ratio − pending_charge)

    ```


    Use `vault_ratio` from the table above (`0.5` on mainnet). Allow submission when `available > 0`. There is no need to mirror the `min_cost` floor on the client — Router enforces it at admission time and returns `402` if the request cannot be covered.


    ## Changelog


    `### Unreleased` lists API changes merged but not yet live on mainnet; on release each is cut into a dated `### vX.Y — YYYY-MM` section. Endpoint and field stability is marked inline with `[beta]`.


    ### Unreleased

    - **`POST /v1/messages` — a model that does not speak the Anthropic wire format now returns `400`, not `503`** — asking `/v1/messages` for a model whose endpoints only serve the OpenAI format used to fail with `503 "No available providers for this request"`, byte-for-byte the error a real outage returns. Since SDKs treat `5xx` as retryable and `4xx` as final, a Messages-API-only client spent its whole retry budget on a request no retry can satisfy — the mismatch is permanent — and then surfaced it as an outage rather than as "use the other endpoint". It is now a `400 invalid_request_error` naming the model and the formats it does serve: `model "some-model" is not available on the anthropic API format (supported: openai); use POST /v1/chat/completions instead`. **If you branch on status codes, add a `400` arm here** — this condition previously arrived as `503`. Check a model''s formats up front via `supported_formats` on `GET /v1/models`. Unchanged: a genuine supply outage still returns `503`, an unknown model still `404`, and a model reachable on the Anthropic format routes exactly as before.

    - **`POST /v1/chat/completions`, `/v1/messages`, `/v1/images/*`, `/v1/audio/transcriptions` — the `ZG-Res-Key` response header now carries the PROVIDER''s response id, passed through verbatim, instead of a router-generated value** — it lets a client independently verify the provider''s TEE signature for that specific response against the provider''s signature endpoint. Two behavioural notes: it is now **present only when the provider returns one** (the router previously always emitted a value), so treat its absence as "no provider response id available" rather than an error; and its value is the provider''s own — opaque and provider-scoped, not a router-owned token. The router''s own stable, always-present per-response identifier is unchanged: read **`X-Request-ID`** (echoed on every response, and the value recorded in your usage history) when you need a handle the router will recognise. No request contract change.

    - **`POST /v1/videos`, `GET /v1/models` — documented: `size` and `seconds` do not mean what the OpenAI Video API means by them** — no behaviour change; the behaviour was undocumented and reads as a bug when you meet it. The request shape is OpenAI''s, but the models behind it are not, and where a model''s own limits differ they win. **`size` names a resolution TIER, not output dimensions**, because a video model advertises the tiers it is PRICED at rather than arbitrary sizes. Two spellings are accepted and they are not equivalent: **pixel dimensions** (`1280x720` — OpenAI''s spelling, and what an SDK sends) select the **aspect ratio only, and only for text-to-video**, so asking a 2K-only model for `1280x720` returns a **2560x1440** clip billed at the 2K rate rather than a 720p one; for **image-to-video they have no effect at all**, since the aspect ratio follows your reference image (a tier name still selects the tier there). **A tier name** (`2K`) addresses the tier directly — send only names that appear in that list, since it is the set we can price and it grows as a model adds tiers. Pixel dimensions are the safe thing to send blind; a tier name is what to send when you must have a specific tier. Read the tiers from `pricing.variants[].dimensions.resolution` on `GET /v1/models`. **`seconds` is clamped to the model''s supported range in BOTH directions, silently**: below the minimum you get and pay for the minimum, above the maximum you get the maximum, and neither errors — so a request outside the range costs something other than what you asked for. Omitting either field is the recommended default. For both, the **submit** response echoes what you sent while the **poll** response reports what was actually rendered, so reconcile against the poll. Per-model limits (supported range, tiers, prompt length, accepted reference-image formats and dimensions) are stated in each model''s `description`; a request that violates one is rejected by the model provider, so that error text and any numeric code in it are theirs. Billing is unaffected throughout — you are billed for the clip actually produced, at the per-second price published for its tier. **[beta]**

    - **`GET /v1/videos/{id}`, `GET /v1/async/jobs/{id}` — fixed: `x_0g_trace.billing` on a re-poll now reports the amount you were CHARGED, not a fresh quote** — an async job''s trace is returned on every poll, not only the one that settles the charge. Polls after settlement were re-deriving the fee at the CURRENT price instead of reading the booked figure, so the reported cost could drift away from the charge over time. It drifts whenever the provider prices in USD, because its native-token rate is derived from a moving FX pair: one clip''s trace read `6794472400000000000` when it settled and `6696290550000000000` five hours later, a 1.45% gap against a charge that had not changed. **No money was ever wrong** — the ledger, `GET /v1/account/usage/history` and your balance always agreed, and every one of them still does — but the trace is what many clients reconcile spend against, so it now carries the booked amount. The wire shape is unchanged (`input_cost` / `output_cost` / `total_cost`, `currency` on USD traces), and the settling poll is unaffected: it already reported the charge it had just written. Reconciling off `GET /v1/account/usage/history` was, and remains, the authoritative route. If the booked figure cannot be read the response falls back to the previous behaviour rather than failing your poll.

    - **`POST /v1/videos`, `GET /v1/videos/{id}`, `GET /v1/videos/{id}/content` — async video generation** — three new endpoints add video as a modality, in the OpenAI Video API shape. `POST /v1/videos` accepts `{model, prompt, seconds, size}` as JSON or as `multipart/form-data` (so an uploaded first-frame image can drive image-to-video) and returns `{id, status, provider_address}`; `GET /v1/videos/{id}` reports status and is what settles the charge once the job completes; `GET /v1/videos/{id}/content` streams the finished file. On both `GET`s **`provider_address` is optional** — it is resolved from the job id — so an OpenAI-native client can poll and download with no query parameters. Because generation takes minutes, billing happens at completion rather than at submit, and on the **duration actually delivered** — your requested `seconds` becomes the billing basis only when the provider''s own figure is unusable — it completed the job but reported no duration at all (so that a delivered clip is never served free), or it reported one implausibly larger than you asked for, which is capped. Both cases are logged as degraded. The cap bounds the DURATION at a small multiple of what you requested, not the fee: on a `video_clip` table the price is a step function of duration, so a capped duration can still land on a dearer row than the one you asked for. The same precedence applies to `size`: the resolution the price is keyed on is the one the provider reports, falling back to the `size` you asked for when the provider does not echo it, which is the normal case for some upstreams — so a resolution-priced model is billed at its own tier rather than at the table''s most expensive one. Two consequences worth coding against: the content endpoint **charges before it streams** if you never polled, so bytes are never delivered unbilled; and each account may hold only a small number of unfinished video jobs at once, so a submit can return **`429`** with code `video_jobs_in_flight_limit` while earlier clips are still rendering. **`seconds` is REQUIRED** — the one place this endpoint deviates from the upstream OpenAI Video API, where it is optional. Billing is per delivered second and the delivered figure comes from the provider, so your declared duration is the only reference the router can sanity-check it against; without one, a provider misreporting its units could charge orders of magnitude more. Omitting it returns `400` `invalid_request` with "seconds is required" — note this applies to the OpenAI SDK path too, including multipart image-to-video. A `seconds` value the model has no price for is likewise rejected up front with `400` and the list of durations that are available, rather than failing after the clip was generated. Other job states: **`404`** `async_job_not_found` on either `GET` means this router has no such job id — it never existed, or you pinned a `provider_address` other than the one that owns it — so re-check the id (and drop the pin, since it is resolved for you) rather than retrying; **`409`** `video_not_ready` from the content endpoint means keep polling; **`424`** `video_job_failed` means the job died at the provider and has no output. A USD-settling account can use video only when the model publishes a USD video price (`pricing_usd.video` or `pricing_usd.variants`); otherwise submit returns `501` `usd_not_supported` rather than a mis-priced charge. **[beta]**

    - **`GET /v1/account/usage/history` — `request_id` VALUE format differs for VIDEO usage rows** — the `request_id` on a usage row for a video job is `async-<16 hex>-<job id>`, while every other async modality uses `async-<8 chars of the provider address>-<job id>`. The field, its type and the `async-` prefix are unchanged, so a `LIKE ''async-%''` filter or an exact-match lookup still behaves identically. Only one thing breaks: if you PARSE the middle segment expecting the provider''s address suffix, it will not match on a video row — read `provider_address` from the row itself instead, which is the stable way to get it for every modality. Nothing else about the field changed, and no other endpoint''s `request_id` changed. **[beta]**

    - **`GET /v1/models`, `GET /v1/providers` — `pricing.video` and `pricing.variants`; `GET /v1/service-types` — `video-generation`** — the pricing object gains **`video`**, the flat price per generated second for a single-rate video model, and **`variants`**, a list of priced request shapes for a model whose price depends on the shape of the request rather than a single rate. Each variant carries `dimensions` (the axes it is keyed on, e.g. `{"resolution":"2K","duration_seconds":"5"}`), `unit` — **`video_second`** (multiply `unit_price` by the generated seconds) or **`video_clip`** (`unit_price` is the whole-clip total) — and `unit_price`, which is always the final per-unit price and never a multiplier to apply yourself. When `variants` is present, use it, and note that a table has one of two shapes, with different fallback rules for a request it does not name. The shape is told by the `dimensions` keys, not by `unit`. A **resolution-only** table (rows keyed on `{"resolution": ...}`) keeps `video` published: it is the rate for any resolution the table does NOT list, so match your resolution against `variants`, else use `video` × seconds. A **bucketed** table (rows keyed on `{"resolution", "duration_seconds"}`) omits `video`, because it is never the basis there, and a request the table does not name exactly is billed off the table itself — the row for **your resolution with the smallest `duration_seconds` that is still ≥ your clip''s duration**, and if no row at your resolution covers it (or your resolution has no rows at all) the **highest-priced row in the whole table**. That last case can therefore be much dearer than the shape you asked for; it means the operator has not tabulated what you requested, and the router counts it so they can. A model with no `variants` at all bills `video` × generated seconds. Resolution matching is case- and whitespace-insensitive on our side. Duration here is the length actually delivered, which for a request carrying a reference image or video is the vendor''s billed length (input + output) and so can exceed the `seconds` you asked for. `GET /v1/service-types` lists `video-generation` / "Video Generation" once a video provider is on the network. **Purely additive — no existing field changes.** A video-generation model still reports `prompt` / `completion`, unchanged. Note these are only per-TOKEN prices for chat: on every other modality `completion` is an echo of the on-chain output price whose meaning follows the modality — per image for `text-to-image` / `image-editing` (which is why `image` exists), and per generated second for `video-generation` (which is why `video` / `variants` exist). So compute video cost from `video` / `variants`, exactly as image cost is computed from `image`; do not multiply `completion` by a token count. One caveat specific to `variants`, because it is a whole price LIST rather than a single rate: `GET /v1/models` aggregates a model across every endpoint serving it and shows one endpoint''s block, so when two endpoints of the same model publish different tables the quote you read may not be the table the request is billed against. Endpoints are per-provider, so read `GET /v1/providers` for the exact list, or pin `provider.address` if you need the quote and the charge to be the same table. This is a general property of the aggregated view rather than something new — the shown block always comes from one endpoint, and which endpoint the request routes to depends on the routing preferences you send (`sort`, a pinned `address`) and on health — but it is worth stating for `variants` specifically, because a table is a whole price list rather than a single rate, so two endpoints can differ in the SHAPE of what they charge and not just the amount. **[beta]**


    - **All inference endpoints — the `min_cost` admission floor rises from `0.00001 0G` to `0.01 0G`** — a request is admitted only when the account can cover `min_cost`, either from the Router ledger or through the vault deferred path (see the **Admission rule** table above, which now reads `0.01 0G`). The practical effect is that an account whose spendable balance has fallen below `0.01 0G` now receives `402` rather than being served one last request. **No balance is lost** — the remainder stays on the account and becomes spendable again on the next top-up — and accounts with vault funds are unaffected, because the vault side of the admission rule is unchanged. The old floor was low enough to be indistinguishable from zero, which let a nearly-empty account be served a request it could not pay for, leaving a debt (`pending_charge`) that the account could then only clear by funding its vault.

    - **`POST /v1/chat/completions`, `/v1/messages`, `/v1/images/*`, `/v1/audio/transcriptions`, `/v1/async/images/*`, `/v1/routing/preview` — pinning a provider that doesn''t serve the requested model now returns `400`, not `500`** — when a request pins a specific provider (`provider.address` in the body or the `X-0G-Provider-Address` header) whose address exists but does not serve the requested `model` (or whose service type / API format doesn''t match the endpoint), the router now returns **`400`** with error code **`provider_model_mismatch`** instead of a generic `500` / `502`. This is a deterministic bad pin — two constraints ("use this exact provider" and "serve this model") that don''t intersect — so retrying won''t help and the response now says so, letting the caller correct the pin. An unpinned request is unaffected (it routes normally to a provider that serves the model), as is a pin that resolves to no provider at all (still `400`/`provider not found`).

    - **`GET /v1/account`, `POST /v1/account/onboarded` (JWT; `GET` also accepts a management key with `account:read`) — account onboarding state** — a new endpoint pair exposes whether the authenticated wallet has handled the new-user onboarding flow. `GET /v1/account` returns account metadata `{address, created_at, onboarded_at}`, where **`onboarded_at`** is an RFC3339 timestamp once the account has completed or dismissed onboarding and **`null`** until then, so a client can decide whether to show the flow. Because the value is stored per wallet on the server (not in the browser), it persists across a cleared cache, another browser, and another device. `POST /v1/account/onboarded` records that the flow was handled, setting `onboarded_at` to the server time; it is **idempotent** — the first call stamps the timestamp and later calls (a client retry, or a "dismiss" after a "complete") leave it unchanged — so both client paths can safely call and retry it. **[beta]**

    - **`POST /v1/chat/completions`, `/v1/messages`, `/v1/images/*`, `/v1/audio/transcriptions`, `/v1/async/images/*` — transient vault-check failure now returns retryable `503` instead of `402`** — a routing-mode (Payment Layer vault) user''s request is admitted by checking their on-chain vault balance. When that chain read fails transiently (RPC blip), the pre-request balance gate previously returned `402 "Insufficient balance"` — indistinguishable from a genuinely empty account, so a funded user saw "insufficient balance" and assumed their deposit was lost. It now returns **`503`** with error code **`vault_unavailable`** (OpenAI-style) / `api_error` (Anthropic-style) and is safe to retry; the next request self-heals once the RPC recovers. Behavior is still fail-closed (the request is never admitted while vault state is unknown — no unbounded deferred debt) and a genuine balance shortfall (vault read succeeds, funds insufficient) is unchanged at `402`. No request contract change.

    - **All endpoints — a client-supplied `X-Request-ID` is now validated before it is echoed and recorded** — the header is still propagated as your correlation id, but only when it is at most 64 characters, built from letters, digits and `- _ . :`, and does not begin with `async-`. A value failing any of those is replaced by a server-generated id, exactly as an over-long one already was; you always get the id actually in effect back in the `X-Request-ID` response header, so read it there rather than assuming your value was kept. UUIDs, ULIDs, hex trace ids and W3C `traceparent` values are unaffected. The `async-` prefix is reserved because the router derives its own asynchronous-billing identifiers in that namespace.'
  title: 0G Router Provider API
  termsOfService: https://0g.ai/terms
  contact:
    name: 0G Labs
    url: https://0g.ai
    email: contact@0g.ai
  version: '1.0'
servers:
- url: /v1
tags:
- name: Provider
paths:
  /providers:
    get:
      description: 'Get all TEE-acknowledged providers, optionally narrowed by service type, on-chain model id, and/or canonical id. `model` and `canonical_id` are independent filters and compose (ANDed): `canonical_id` alone lists every endpoint of a canonical, while `model` + `canonical_id` narrows to a specific endpoint within it. An empty value means "not filtered on" (so no filters = list all). Includes providers with unknown health status (is_healthy=null).'
      tags:
      - Provider
      summary: Get provider list
      parameters:
      - description: Filter by service type (chatbot, text-to-image, speech-to-text)
        name: service_type
        in: query
        schema:
          type: string
      - description: Filter by on-chain model ID (e.g. zai-org/GLM-5.1-FP8); exact model_id match. Empty = not filtered.
        name: model
        in: query
        schema:
          type: string
      - description: '[beta] Filter by canonical model ID (e.g. glm-5.1); lists every endpoint serving that canonical. Composes (AND) with model. Empty = not filtered.'
        name: canonical_id
        in: query
        schema:
          type: string
      responses:
        '200':
          description: Provider list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/github_com_0glabs_0g-router_pkg_response.ProviderListResponse'
        '500':
          description: Server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/github_com_0glabs_0g-router_pkg_response.ErrorResponse'
components:
  schemas:
    github_com_0glabs_0g-router_pkg_response.ModelPricingTier:
      type: object
      properties:
        cached_prompt:
          type: string
        completion:
          type: string
        max_input_tokens:
          type: integer
        prompt:
          type: string
    github_com_0glabs_0g-router_pkg_response.ModelArchitecture:
      type: object
      properties:
        input_modalities:
          type: array
          items:
            type: string
        instruct_type:
          type: string
        modality:
          type: string
        output_modalities:
          type: array
          items:
            type: string
        tokenizer:
          type: string
    github_com_0glabs_0g-router_pkg_response.ProviderEntry:
      type: object
      properties:
        address:
          type: string
          example: '0x1234567890abcdef'
        architecture:
          $ref: '#/components/schemas/github_com_0glabs_0g-router_pkg_response.ModelArchitecture'
        canonical_id:
          description: '[beta] Canonical model id this endpoint serves; empty if unmapped'
          type: string
          x-stability: beta
          example: glm-5.1
        context_length:
          type: integer
        default_parameters:
          type: object
          additionalProperties: {}
        expiration_date:
          description: Upstream model availability expiration (RFC3339); empty = no expiration. Only visible before the instant — an expired endpoint drops out of this listing entirely.
          type: string
        is_healthy:
          description: nil=unknown, true=healthy, false=unhealthy
          type: boolean
          example: true
        latency:
          description: nil=no health data
          type: integer
          example: 150
        max_completion_tokens:
          type: integer
        model_id:
          type: string
          example: qwen-2.5-7b-instruct
        name:
          description: Model metadata (populated when listing providers for a specific model)
          type: string
        pricing:
          $ref: '#/components/schemas/github_com_0glabs_0g-router_pkg_response.ModelPricing'
        pricing_usd:
          description: Per-token USD prices; only set when upstream metadata provides them
          allOf:
          - $ref: '#/components/schemas/github_com_0glabs_0g-router_pkg_response.ModelPricing'
        provider_country:
          description: '[beta] Operator-declared serving country/locality'
          type: string
          x-stability: beta
        provider_name:
          description: 'Operator-declared, descriptive endpoint identity/locality (from the

            broker''s /models metadata). Empty when the operator did not declare them.'
          type: string
          x-stability: beta
        service_type:
          type: string
          example: chatbot
        serving_domain:
          description: '[beta] Operator-declared serving domain'
          type: string
          x-stability: beta
        supported_formats:
          type: array
          items:
            type: string
        supported_parameters:
          type: array
          items:
            type: string
        tee_acknowledged:
          type: boolean
          example: true
        tee_attested:
          type: boolean
        tee_type:
          type: string
        tee_verifier:
          type: string
        trust_mode:
          description: '[beta] Routable trust tier of this endpoint: standard | verified | private. Derived from provider_type — the authoritative tier signal (verifiability is a descriptive TEE label and is empty for standard). Use this to match a request''s provider.trust_mode / X-0G-Provider-Trust-Mode floor.'
          type: string
          x-stability: beta
        type:
          type: string
        uptime:
          description: nil=no health data
          type: number
          example: 99.5
        verifiability:
          type: string
    github_com_0glabs_0g-router_pkg_response.ModelPricing:
      type: object
      properties:
        cache_write:
          description: 'CacheWrite is the per-token price for writing a prompt token to the cache

            (cache creation) at the default/5-minute TTL, i.e. prompt * write_multiplier.

            [beta] Present only when the provider advertises a cache-write premium

            (write_multiplier > 1) and features.cache_write_premium is enabled; omitted

            otherwise.'
          type: string
        cache_write_1h:
          description: 'CacheWrite1h is the per-token price for writing a prompt token to the cache

            at the 1-hour TTL, i.e. prompt * write_1h_multiplier. [beta] Present only

            when the provider advertises a distinct, valid 1-hour cache-write premium and

            features.cache_write_premium is enabled; omitted otherwise (1-hour writes

            then bill at CacheWrite, or the plain prompt rate when no premium applies).'
          type: string
        cached_prompt:
          type: string
        completion:
          type: string
        image:
          type: string
        prompt:
          type: string
        tiered_pricing:
          type: array
          items:
            $ref: '#/components/schemas/github_com_0glabs_0g-router_pkg_response.ModelPricingTier'
        variants:
          type: array
          items:
            $ref: '#/components/schemas/github_com_0glabs_0g-router_pkg_response.ModelPriceVariant'
        video:
          description: 'Video is the flat per-effective-output-second price for a single-rate

            video-generation model (wei in the native block, USD decimal in the USD

            block). Variants supersedes it when the model prices by request shape

            (resolution/duration) — see ModelPriceVariant. Both mirror the broker''s

            /models pricing.video / pricing.variants so a video model shows a real

            per-second/per-resolution price instead of a misleading per-token one.'
          type: string
    github_com_0glabs_0g-router_pkg_response.ErrorResponse:
      type: object
      properties:
        error:
          $ref: '#/components/schemas/github_com_0glabs_0g-router_pkg_response.ErrorDetail'
        request_id:
          type: string
    github_com_0glabs_0g-router_pkg_response.ProviderListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/github_com_0glabs_0g-router_pkg_response.ProviderEntry'
        object:
          type: string
          example: list
    github_com_0glabs_0g-router_pkg_response.ErrorDetail:
      type: object
      properties:
        code:
          type: string
          example: bad_request
        message:
          type: string
          example: Invalid request
        type:
          type: string
          example: invalid_request
    github_com_0glabs_0g-router_pkg_response.ModelPriceVariant:
      type: object
      properties:
        dimensions:
          type: object
          additionalProperties:
            type: string
        unit:
          type: string
        unit_price:
          type: string
  securitySchemes:
    ApiKeyAuth:
      description: 'API key for inference, file upload, and async image endpoints. Format: "Bearer sk-..."'
      type: apiKey
      name: Authorization
      in: header
    ManagementKeyAuth:
      description: 'Management key for account read and API key management. Capabilities are scope-limited per key (account:read, keys:read, keys:create, keys:manage). Format: "Bearer mk-..."'
      type: apiKey
      name: Authorization
      in: header