0G Labs API Key API

The API Key API from 0G Labs — 2 operation(s) for api key.

OpenAPI Specification

0g-labs-api-key-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 API Key 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: API Key
paths:
  /api-keys:
    get:
      security:
      - ManagementKeyAuth: []
      description: Get all API keys for current user
      tags:
      - API Key
      summary: Get API key list
      parameters:
      - description: 'Filter: active / revoked / expired / all (default all)'
        name: status
        in: query
        schema:
          type: string
      responses:
        '200':
          description: API key list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/github_com_0glabs_0g-router_pkg_response.APIKeyListResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/github_com_0glabs_0g-router_pkg_response.ErrorResponse'
        '500':
          description: Server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/github_com_0glabs_0g-router_pkg_response.ErrorResponse'
    post:
      security:
      - ManagementKeyAuth: []
      description: Create a new API key for LLM service access
      tags:
      - API Key
      summary: Create API key
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/github_com_0glabs_0g-router_pkg_request.CreateAPIKeyRequest'
        description: API key configuration
      responses:
        '200':
          description: API key created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/github_com_0glabs_0g-router_pkg_response.CreateAPIKeyResponse'
        '400':
          description: Request error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/github_com_0glabs_0g-router_pkg_response.ErrorResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/github_com_0glabs_0g-router_pkg_response.ErrorResponse'
        '500':
          description: Server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/github_com_0glabs_0g-router_pkg_response.ErrorResponse'
  /api-keys/{keyId}:
    delete:
      security:
      - ManagementKeyAuth: []
      description: Revoke a specific API key
      tags:
      - API Key
      summary: Revoke API key
      parameters:
      - description: API Key ID
        name: keyId
        in: path
        required: true
        schema:
          type: string
      responses:
        '200':
          description: Revoked successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/github_com_0glabs_0g-router_pkg_response.RevokeAPIKeyResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/github_com_0glabs_0g-router_pkg_response.ErrorResponse'
        '404':
          description: API key not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/github_com_0glabs_0g-router_pkg_response.ErrorResponse'
        '500':
          description: Server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/github_com_0glabs_0g-router_pkg_response.ErrorResponse'
    patch:
      security:
      - ManagementKeyAuth: []
      description: Update name / credit_limit / reset_period / expiration of an API key
      tags:
      - API Key
      summary: Update API key
      parameters:
      - description: API Key ID
        name: keyId
        in: path
        required: true
        schema:
          type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/github_com_0glabs_0g-router_pkg_request.UpdateAPIKeyRequest'
        description: Patch payload
        required: true
      responses:
        '200':
          description: Updated key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/github_com_0glabs_0g-router_pkg_response.APIKeyItem'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/github_com_0glabs_0g-router_pkg_response.ErrorResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/github_com_0glabs_0g-router_pkg_response.ErrorResponse'
        '404':
          description: API key not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/github_com_0glabs_0g-router_pkg_response.ErrorResponse'
        '409':
          description: Duplicate name
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/github_com_0glabs_0g-router_pkg_response.ErrorResponse'
        '410':
          description: Key revoked
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/github_com_0glabs_0g-router_pkg_response.ErrorResponse'
        '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.APIKeyListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/github_com_0glabs_0g-router_pkg_response.APIKeyItem'
        object:
          type: string
          example: list
    github_com_0glabs_0g-router_pkg_response.CreateAPIKeyResponse:
      type: object
      properties:
        allowed_models:
          description: 'AllowedModels / AllowedProviders are ALWAYS present in the response

            (never omitted) — `[]` means "no restriction", a non-empty slice

            means "only these". Stable shape so clients can index without

            `field !== undefined` guards, and so a missing field cleanly

            signals "response truncated by middleware / bug" rather than "no

            allowlist". csvToSlice in the service layer materialises an empty

            slice (not nil) for the no-allowlist case to keep the contract.'
          type: array
          items:
            type: string
          example:
          - '["glm-5"]'
        allowed_providers:
          type: array
          items:
            type: string
          example:
          - '["0xabc..."]'
        created_at:
          type: string
          example: 2024-01-15 10:30:00+00:00
        credit_limit:
          type: string
          example: '1.5'
        currency:
          description: 'Currency is the unit of CreditLimit / Used: "0g" (0G decimals) or "usd"

            (USD decimals), following the account''s settlement mode. Without it a

            client can''t tell whether "1.5" means 1.5 0G or $1.50.'
          type: string
          example: 0g
        expires_at:
          type: string
          example: 2025-02-15 10:30:00+00:00
        key:
          description: Only shown once!
          type: string
        key_id:
          type: string
          example: abc12345
        key_preview:
          type: string
          example: sk-abcde…
        name:
          type: string
          example: my-api-key
        reset_period:
          type: string
          example: monthly
        revoked:
          type: boolean
          example: false
        status:
          description: active / revoked / expired
          type: string
          example: active
        trust_mode:
          description: 'TrustMode is the trust-tier pin for this key, or nil when the key

            has no pin (matches the API-key constraint table verbatim). Emitted

            as a pointer so callers can distinguish "no pin" from "pinned to

            the empty string"; the latter is rejected at create time, so a nil

            value is the only way the field is absent in practice.'
          type: string
          example: verified
        used:
          type: string
          example: '0.42'
    github_com_0glabs_0g-router_pkg_request.UpdateAPIKeyRequest:
      type: object
      properties:
        allowed_models:
          description: "AllowedModels / AllowedProviders sparse-PATCH sentinels:\n  nil               → unchanged\n  non-nil empty []  → clear the allowlist (no restriction)\n  non-nil non-empty → replace the allowlist\n\nPointer to slice (rather than slice) so we can distinguish \"leave\nalone\" from \"clear\", matching how Scopes is handled on mgmt keys."
          type: array
          items:
            type: string
        allowed_providers:
          type: array
          items:
            type: string
        credit_limit:
          type: string
        expiration:
          type: string
        name:
          type: string
        reset_period:
          type: string
        trust_mode:
          description: 'TrustMode sparse-PATCH sentinels: nil → unchanged; "" → clear the

            pin (key falls back to no trust-mode restriction); any of

            standard|verified|private → set the pin.'
          type: string
    github_com_0glabs_0g-router_pkg_response.APIKeyItem:
      type: object
      properties:
        allowed_models:
          description: 'AllowedModels / AllowedProviders are ALWAYS present in the response

            (never omitted) — `[]` means "no restriction", a non-empty slice

            means "only these". Stable shape so clients can index without

            `field !== undefined` guards, and so a missing field cleanly

            signals "response truncated by middleware / bug" rather than "no

            allowlist". csvToSlice in the service layer materialises an empty

            slice (not nil) for the no-allowlist case to keep the contract.'
          type: array
          items:
            type: string
          example:
          - '["glm-5"]'
        allowed_providers:
          t

# --- truncated at 32 KB (36 KB total) ---
# Full source: https://raw.githubusercontent.com/api-evangelist/0g-labs/refs/heads/main/openapi/0g-labs-api-key-api-openapi.yml