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