omnigate

Building on omnigate

omnigate is a model gateway. What it does inside a request (routing, failover, translation, redaction, metering) is the project's own code, and changes there are made in the repository. Everything else is meant to be built around it, over HTTP, by programs that omnigate knows nothing about: a payment service that credits keys, a billing tool that reads usage, an agent harness that calls models.

This page is for the people writing those programs. It names the interfaces you can rely on, shows the three most common things built around a gateway, and says plainly what is not offered.

Three runnable examples go with it, in examples/, Python standard library only. The end-to-end test (tools/e2e.py) runs each of them against the mock providers on every change, so they do not rot:

File What it is
examples/agent_client.py A tool-calling loop against the gateway, with the handling of 402, 429 and 5xx a client owes it
examples/prepaid_topup.py The skeleton of a payment service: hears of payments, credits keys, safe to restart
examples/usage_export.py Pages the usage feed into a CSV file, continuing where it stopped

The interfaces

Interface Port Who calls it Reference
Gateway API: the OpenAI and Anthropic text endpoints, /v1/models, the jobs API, file URLs data plane (8080) applications, agents, SDKs, with a gateway key API
Admin API: keys, top-ups, the usage feed, the audit log control plane (4000) your own back-office programs, with the admin token Admin
Webhooks: a signed POST when a media job finishes from the control plane to a URL you give your receiver API
Metrics: Prometheus text control plane, GET /metrics your monitoring API

There is no fifth one. In particular there is no plugin API and no hook that runs your code inside a request; see What is not offered.

Agents, harnesses and apps

Anything that takes a base URL and an API key works, because the gateway speaks the providers' own protocols:

Client speaks Base URL Key goes in
OpenAI (Chat Completions, Responses, embeddings) http://<gateway>:8080/v1 Authorization: Bearer <gateway key>
Anthropic (Messages) http://<gateway>:8080 x-api-key: <gateway key>

"model" is the name of a route in the gateway's config, not a provider's model id. Which provider answers, and in which protocol, is the gateway's business: an OpenAI-style client can be routed to an Anthropic model and the other way round (translation). The official SDKs are covered in Using the official SDKs.

One key per agent, tenant or end user. A gateway key is the unit everything is enforced and counted by. Create one with POST /admin/keys (Admin) and give it:

Field What it guards against
budget_usd with budget_period An agent that loops: the key is answered 402 budget_exceeded once the budget is spent
rpm, max_concurrent A burst: 429 with retry-after
models Using a route it was not meant to use: 403
max_open_jobs, max_jobs_per_day Media jobs piling up
privacy What a provider may see and what is kept: Privacy mode

These hold whatever the agent does, because they are enforced by the gateway and not by the agent's own code.

What a client owes the gateway. Three answers deserve handling, and examples/agent_client.py shows all three:

Answer Meaning Do
402, code budget_exceeded The key's budget is spent Stop and report. Retrying cannot help until the key is topped up or its period turns
429 A rate limit: the key's own (rate_limit_exceeded, concurrency_limit_exceeded) or every provider's (upstream_rate_limited) Wait for the retry-after header, then try again
5xx Every target failed, or the gateway is overloaded Back off and try again; the error lists the attempts

Every response carries x-omnigate-request-id. It is the id of that request in the usage feed, so a client can log it and a billing tool can find the same request later.

Tagging requests. A client may send x-omnigate-tag: <label> on a text request or on POST /v1/jobs: its own name for whose request this is (a tenant, a run, an end user's id in your system). The gateway records it with the request's usage and does nothing else with it:

curl http://127.0.0.1:8080/v1/chat/completions -H "Authorization: Bearer og-unlimited-key" -H "x-omnigate-tag: tenant-42" -d '{"model":"demo-chat","messages":[{"role":"user","content":"hi"}]}'

Taking payments: prepaid keys and top-ups

omnigate has a budget per key and one call that adds to it safely. A payment service of any kind (a chain watcher, a card processor's webhook receiver, a manual back-office tool) is a small program next to the control plane, and this section is how to write one.

(There is now also one payment provider built in, opt-in: with a payments section in the config, key holders buy credit themselves in crypto through NEAR Intents, with no program of yours in between. See Payments. It credits keys through the same top-up described here, and everything else on this page works next to it. What credit burns and earns, whoever paid for it, is in Prepaid credit.)

  1. A customer gets a key with an empty prepaid budget. A total budget is credit bought in advance: it does not start over by itself.

    curl http://127.0.0.1:4000/admin/keys -H "Authorization: Bearer dev-admin-token-change-me" -H "Content-Type: application/json" -d '{"name":"customer-42","budget_usd":0}'
    

    The answer contains the key itself, once. With a budget of 0 the key is answered 402 until it is topped up.

  2. Each payment is one top-up, named by the payment.

    curl http://127.0.0.1:4000/admin/keys/customer-42/topup -H "Authorization: Bearer dev-admin-token-change-me" -H "Content-Type: application/json" -d '{"amount_usd":5,"reference":"0x9f2c...e1"}'
    
    {
      "key": { "name": "customer-42", "budget_usd": 5.0, "period_spend_usd": 0.0, "state": "active", "...": "..." },
      "topup": { "reference": "0x9f2c...e1", "key": "customer-42", "amount_usd": 5.0, "budget_usd": 5.0, "at": "2026-11-11T18:00:00Z", "replayed": false }
    }
    

    reference is your id for the payment: a transaction hash, the payment provider's id, an invoice number. The budget and the reference are written to disk together. Sending the same top-up again adds nothing and answers with what the first call did, "replayed": true. So your service may deliver each payment at least once (retry on a timeout, rescan after a restart) and omnigate turns that into exactly once.

  3. The key works again within seconds. The data plane learns of the new budget with its next config poll (OMNIGATE_POLL_MS, two seconds by default); no restart.

What can come back, besides 200:

Status and code Meaning Do
400 invalid_request amount_usd is not a number above 0 (at most 1e9), or reference is not 1 to 200 printable ASCII characters without spaces Fix the call; nothing was added
404 key_not_found No such key Create it, or set the payment aside for a person
409 not_prepaid The key has no budget, or a monthly or daily one Top-ups only add to a total budget; give the key one first
409 read_only_key The key is defined in the config file Use a managed key
409 reference_conflict The reference was already used for another key or another amount A bug in your service, or two payments sharing an id: a person must look
5xx, or no answer Nothing is known Send it again later with the same reference

Things to know:

examples/prepaid_topup.py is this service with a file standing in for the payment source. Replace its FileSource with whatever tells you that money arrived; it only has to hand out {reference, key, amount_usd} and may hand out the same payment more than once.

Reading usage

Three views, for three purposes:

You want Use
Every single request and job, and every top-up, to bill or analyse GET /admin/events, below
Totals per key, right now GET /admin/keys (requests, tokens, cost_usd, credits_burned_usd, period_spend_usd, and a prepaid key's credits)
Time series and alerts GET /metrics (omnigate_key_* per key, never per tag)

The usage feed. GET /admin/events returns what the ledger recorded, oldest first: one event per metered text request and one per media job.

curl "http://127.0.0.1:4000/admin/events?limit=2" -H "Authorization: Bearer dev-admin-token-change-me"
{
  "events": [
    {
      "cursor": "1-412", "id": "0d5f...", "kind": "text", "ts": "2026-11-11T18:00:01.120Z", "received_at": "2026-11-11T18:00:01.640Z",
      "key": "customer-42", "tag": "tenant-42", "model": "demo-chat", "upstream": "mock-ok", "target_model": "ok-1",
      "status": 200, "stream": false, "input_tokens": 11, "cached_input_tokens": 0, "cache_write_input_tokens": 0, "output_tokens": 7,
      "outputs": null, "cached": false, "estimated": false, "latency_ms": 31, "cost_usd": 0.0000041, "credits_usd": 0.0000041
    }
  ],
  "next": "1-824",
  "more": true,
  "gap": false
}
Parameter Meaning
after A cursor: the next of an earlier page, or one event's cursor. Left out: from the oldest event still on disk
limit Events per page, 1 to 1000 (default 100)
key Only this key's events, by name, across its rotations. 404 key_not_found for a name that does not exist

How to consume it:

examples/usage_export.py is a complete consumer: it appends to a CSV file and keeps its cursor next to it.

Reacting to media jobs

A job can carry a webhook_url. When it finishes, the control plane sends a signed POST there, with retries (delivery). A receiver must verify the signature against the raw body before trusting it; Verifying the signature has the code. The key's webhook_secret (set on the key, in the config or through PATCH /admin/keys/{name}) is the signing secret.

A job's metadata (up to 16 KiB of your own JSON) comes back in the webhook and when the job is fetched; x-omnigate-tag on POST /v1/jobs puts a label on its event in the usage feed. Use the first for what your receiver needs, the second for what your billing needs.

There are no webhooks for text requests, for a key running out of budget, or for configuration changes. For a budget alert use the metrics above.

Operating notes

What is not offered

If something you need cannot be built from keys, budgets, tags, the usage feed, webhooks and metrics, that is worth an issue: it is how the list of interfaces grows.

What "stable" means before 1.0

omnigate has not reached 1.0, and nothing here is frozen. What the project commits to until then:

Interface Commitment
The gateway's provider-compatible endpoints They follow the providers' own APIs. A difference from those is a bug
The jobs API, file URLs, webhook payloads and signatures Changes are additive where possible; a breaking change is listed in the changelog under "Changed" and in Upgrading
GET/POST/PATCH/DELETE /admin/keys..., POST /admin/keys/{name}/topup, GET /admin/events, GET /admin/audit, GET /metrics The same: fields may be added, and a consumer must ignore fields it does not know. Removing or renaming a field, or changing what one means, is a breaking change and is announced as above
Cursors of GET /admin/events Opaque text. Do not build or take apart one; a cursor stays valid across restarts and upgrades of the same installation
GET /admin/overview, /admin/private-chat/* Not stable. They are the feeds of the dashboard's own pages and change with them. Do not build on them
/internal/* Between the two processes only

"Announced" means: in the changelog entry of the version that changes it and in the upgrade notes, before the release is tagged. It does not mean a deprecation period; before 1.0 there is none.

This page on GitHub