omnigate

Providers

How to configure each provider that works today, and how far each has been tested. The "Status" lines repeat the README's What has been verified section (November 2026); "verified" there means through the gateway, over TLS, with real keys. Nothing has run under sustained or concurrent load against real providers.

After commits e4e30e4 and d79406b, which replaced the control plane's HTTP client and the data plane's response reader: the control plane's new HTTP client and the data plane's new response reader were each run against the real hosts of OpenAI, Anthropic, Gemini, xAI, ElevenLabs, Replicate and fal with deliberately invalid keys; every provider's own auth error came back intact. The successful paths were then re-run with real keys on 11 November 2026, at commit 809b96f, and all passed: OpenAI and Anthropic text (direct, streamed, translated both ways, tool calls, failover, metering), Gemini text, xAI text, an OpenAI image job, Gemini image, speech and video jobs, an xAI image job and a one-second xAI video, a Replicate image job, ElevenLabs speech with a cache hit, and output storage in a Cloudflare R2 bucket. fal has still only returned an auth error, never a completed job. The Sume adapter was added after all of this and has only met a mock.

Read every "Status: verified" line on this page, and the status column of the media table, with that in mind: each records a check made before those two commits, on the client and the reader they replaced. The check with invalid keys changes no status below. It covers no successful request, and the fal adapter is still not verified.

Model ids in the examples marked <...> are placeholders: use an id your account has. Unless an example says otherwise its prices are 0.0; fill in your provider's prices to get spend figures and budgets. Text targets take four prices: input, output, and optionally prompt-cache reads and writes (price_cached_in_per_mtok, price_cache_write_per_mtok), which default to the input price; see Configuration.

Text providers

A text provider is an entry in upstreams[] plus one or more routes that target it. Its key goes in the data plane's environment, under the name in api_key_env. Field reference: Configuration.

The gateway speaks two protocols, openai and anthropic. A client can use either protocol with any upstream on /v1/chat/completions and /v1/messages; see Translation.

For every text provider, the gateway passes the provider's rate-limit, retry-after and request-id response headers on to the client (API), and tells prompt-cache reads and writes apart from ordinary input when it meters a call (Operations). Both are covered by unit tests on OpenAI- and Anthropic-format responses; the "Status" lines below predate them and do not include them.

What a text provider is sent, and how its answer is read, is the same for all of them:

The details are in API. tools/malformed_test.py plays a provider that sends such answers (the special models of tools/mock_upstream.py). What has been run against real providers since this reader replaced the old one is recorded at the top of this page.

OpenAI

{ "name": "openai", "kind": "openai", "base_url": "https://api.openai.com", "api_key_env": "OPENAI_API_KEY" }
{
  "model": "gpt",
  "targets": [
    { "upstream": "openai", "model": "<openai model id>", "price_in_per_mtok": 0.0, "price_out_per_mtok": 0.0 }
  ]
}

Status: verified. Chat completions (gpt-4o-mini, gpt-5.4-mini), streaming, embeddings, the Responses API, provider errors.

Anthropic

{ "name": "anthropic", "kind": "anthropic", "base_url": "https://api.anthropic.com", "api_key_env": "ANTHROPIC_API_KEY" }
{
  "model": "claude",
  "targets": [
    { "upstream": "anthropic", "model": "claude-sonnet-5-5", "price_in_per_mtok": 2.0, "price_out_per_mtok": 10.0 }
  ]
}

(This route is the one in config/omnigate.example.json, prices included; check them against Anthropic's price list.)

The client's anthropic-version and anthropic-beta headers are forwarded; without an anthropic-version the gateway sends 2023-06-01.

Status: verified. Messages (claude-haiku-5-5, claude-opus-5-5 with thinking on), streaming, token counting, provider errors. Translation both ways was verified between OpenAI and Anthropic: text, system prompts, streaming, tool calls, tool-result round trips, streamed tool calls, error shapes, refused sampling parameters.

Current Claude models refuse temperature and top_p. A client speaking the Anthropic protocol gets Anthropic's own error for that. A client speaking the OpenAI protocol (a translated request) gets a retry without them.

Gemini (text)

Google serves an OpenAI-compatible API, but not under /v1. Use an openai upstream with v1_path:

{
  "name": "gemini",
  "kind": "openai",
  "base_url": "https://generativelanguage.googleapis.com",
  "api_key_env": "GEMINI_API_KEY",
  "v1_path": "/v1beta/openai"
}
{
  "model": "gemini",
  "targets": [
    { "upstream": "gemini", "model": "<gemini model id>", "price_in_per_mtok": 0.0, "price_out_per_mtok": 0.0 }
  ]
}

Status: verified with gemini-3.5-flash-lite and gemini-embedding-001. Chat, streaming, tool calls and round trips, embeddings, provider errors, failover to it, metering; and the same from an Anthropic-style client, translated.

Gemini 3 and later demand a "thought signature" on earlier tool calls. An Anthropic-style client cannot carry one, so when a translated request is refused for that reason the gateway retries with the placeholder Google documents and remembers it for that model.

Gemini's image, speech and video models are not served by this endpoint; see Gemini image and speech and Gemini video.

xAI (Grok)

No provider-specific code: an openai upstream.

{ "name": "xai", "kind": "openai", "base_url": "https://api.x.ai", "api_key_env": "XAI_API_KEY" }
{
  "model": "grok",
  "targets": [
    { "upstream": "xai", "model": "<xai model id>", "price_in_per_mtok": 0.0, "price_out_per_mtok": 0.0 }
  ]
}

Status: verified with grok-4.20-0309-non-reasoning. Chat, streaming, tool calls and round trips, the Responses API, provider errors, failover, metering; the same from an Anthropic-style client, translated. The README also records that xAI's own Anthropic-compatible endpoint worked as an anthropic upstream; the repository does not record the base URL used for that.

NEAR AI Cloud (attested)

An openai upstream, with the one thing no other provider here has: an attestation setting. NEAR AI Cloud runs its gateway and the models it serves itself in Intel TDX machines, and the gateway verifies that before a request goes there. All of it: Attested upstreams.

{
  "name": "near-ai",
  "kind": "openai",
  "base_url": "https://cloud-api.near.ai",
  "api_key_env": "NEAR_AI_API_KEY",
  "attestation": { "provider": "near", "accepted_tcb": ["UpToDate"], "gpu": "if_present", "max_age_s": 900 }
}
{ "model": "private", "targets": [ { "upstream": "near-ai", "model": "z-ai/glm-5.3-flash", "price_in_per_mtok": 0.0, "price_out_per_mtok": 0.0 } ] }

Status: verified against the real service on 11 November 2026 (tools/near_live.py). Without a key: gateway and model reports, GPU evidence, re-checks, refusal of a relayed model, routing by the verdict. With a key: three short chat completions through the attested target, one with redaction, one whole and one streamed, the last two with a receipt that verified. Tool calls, the Responses API and metering against NEAR's own bill are untested beyond what any OpenAI-compatible upstream shares. The models are reasoning models: an answer can take tens of seconds, and a small max_tokens can be used up before any text is written.

Other OpenAI-compatible providers

Any service that speaks the OpenAI protocol can be an openai upstream. Set base_url to the part before /v1, and add v1_path if the API is not under /v1. The LiteLLM importer (tools/import_litellm.py) carries this table of base URLs:

Provider base_url v1_path Default key variable
Groq https://api.groq.com/openai GROQ_API_KEY
Mistral https://api.mistral.ai MISTRAL_API_KEY
DeepSeek https://api.deepseek.com DEEPSEEK_API_KEY
OpenRouter https://openrouter.ai/api OPENROUTER_API_KEY
Together https://api.together.xyz TOGETHERAI_API_KEY
Fireworks https://api.fireworks.ai/inference FIREWORKS_API_KEY
Perplexity https://api.perplexity.ai "" (API at the root) PERPLEXITYAI_API_KEY
Ollama (local) http://127.0.0.1:11434 none
LM Studio (local) http://127.0.0.1:1234 none

Status: none of the providers in this table appears in the README's list of verified providers. Treat them as untested until you have tried them.

api_key_env is required in the config even for a local server that needs no key; name any variable.

Example:

{ "name": "groq", "kind": "openai", "base_url": "https://api.groq.com/openai", "api_key_env": "GROQ_API_KEY" }

Failover across text providers

List several targets in one route. Verified per the README: failover from a dead OpenAI-style upstream to Claude, with ledger token totals equal to what the providers reported.

{
  "model": "chat",
  "targets": [
    { "upstream": "openai", "model": "<openai model id>", "price_in_per_mtok": 0.0, "price_out_per_mtok": 0.0 },
    { "upstream": "anthropic", "model": "claude-sonnet-5-5", "price_in_per_mtok": 2.0, "price_out_per_mtok": 10.0 }
  ]
}

Media providers

A media provider is an entry in media.upstreams[] plus one or more media.routes[] that target it. Its key goes in the control plane's environment. Field reference: Configuration.

Clients use all of them through the same Jobs API; only the contents of input differ per adapter. Common behaviour:

The duration, input and multiplier settings are covered by the control plane's unit tests (control/test/wave6_test.exs) against a stub provider. None of them has been used with a real provider.

kind Provider API Style Status
replicate Replicate predictions submit and poll verified
fal fal queue submit and poll mock only
openai_images OpenAI image generation one call verified
elevenlabs ElevenLabs speech, sound effects, music (one call each); images and videos (submit and poll) by the target's api speech, sound and music verified; a successful image or video job mock only (see ElevenLabs)
gemini Gemini generateContent (image, speech) one call verified
gemini_interactions Gemini Interactions API (video) one call, or submit and poll verified in the foreground; background polling mock only
xai_video xAI video generation submit and poll verified
sume Sume image and video routers submit and poll real image and video job; cancel and refusals mock only

Replicate

{ "name": "replicate", "kind": "replicate", "base_url": "https://api.replicate.com", "api_key_env": "REPLICATE_API_TOKEN" }
{
  "model": "image",
  "timeout_s": 300,
  "targets": [
    { "upstream": "replicate", "model": "<owner/name or owner/name:version>", "price": { "per_output_usd": 0.0 } }
  ]
}

The target model is owner/name (the gateway calls POST /v1/models/{owner}/{name}/predictions) or owner/name:version (it calls POST /v1/predictions with that version). The job's input is sent as the prediction's input, so it takes whatever the Replicate model takes:

curl http://127.0.0.1:8080/v1/jobs -H "Authorization: Bearer <gateway key>" -d '{"model":"image","input":{"prompt":"a lighthouse at dusk"}}'

The prediction is polled until it is succeeded, failed or canceled. Its output (a URL or a list of URLs) becomes the job's outputs.

Status: verified with black-forest-labs/flux-1.1-pro: prediction created and polled to completion, the image copied off Replicate and downloaded from the gateway, webhook delivered; an unknown model and an out-of-credit model failed with Replicate's own reasons. Not verified: the owner/name:version form, and job cancellation at a real provider.

fal

{ "name": "fal", "kind": "fal", "base_url": "https://queue.fal.run", "api_key_env": "FAL_KEY" }
{
  "model": "video",
  "timeout_s": 900,
  "targets": [
    { "upstream": "fal", "model": "<fal video model id>", "price": { "per_second_usd": 0.0 }, "default_seconds": 5, "min_seconds": 1, "max_seconds": 10 }
  ]
}

(As in config/omnigate.example.json. The three _seconds settings take effect once per_second_usd is above 0.)

The job's input is sent as the body of POST {base_url}/{model}. The queue is polled until COMPLETED; every object with a url field in the result (images[], video, audio and so on) becomes an output. The status, result and cancel URLs fal returns are followed only when they are under the configured base_url.

curl http://127.0.0.1:8080/v1/jobs -H "Authorization: Bearer <gateway key>" -d '{"model":"video","input":{"prompt":"waves","duration":5}}'

With per_second_usd, the price is multiplied by the larger of input.duration and input.seconds (each a number, a numeric string or a number followed by s), or by the target's default_seconds when the job names neither, and never by less than min_seconds; see Media providers above for when a job is refused. With per_input_char_usd, the characters of input.prompt, input.text and input.input are added together, because the whole input is sent. The same holds for the Replicate and xAI video adapters.

Status: not verified. The fal adapter has only run against a mock written from public documentation; expect details to differ.

OpenAI images

{ "name": "openai-images", "kind": "openai_images", "base_url": "https://api.openai.com", "api_key_env": "OPENAI_API_KEY" }
{
  "model": "image-sync",
  "targets": [
    { "upstream": "openai-images", "model": "<openai image model id>", "price": { "per_output_usd": 0.0 } }
  ],
  "allowed_input": ["prompt", "size", "quality", "background", "output_format"],
  "price_multipliers": {
    "quality": { "low": 1, "medium": 4, "high": 16, "default": 16 },
    "size": { "1024x1024": 1, "1024x1536": 1.5, "1536x1024": 1.5, "default": 1.5 }
  }
}

(As in config/omnigate.example.json. Check the fields and the factors against your provider's documentation and price list.)

One synchronous POST /v1/images/generations. The job's input is the request body, with model set to the target's model. Results may come back as b64_json or as a url; both are stored. For base64 results the stored content type follows input.output_format (jpeg, webp, otherwise PNG).

Because the whole input is forwarded, a client can ask for options that change what the provider charges. With the route above:

curl http://127.0.0.1:8080/v1/jobs -H "Authorization: Bearer <gateway key>" -d '{"model":"image-sync","input":{"prompt":"a red cube"}}'

The same adapter works for other providers with an OpenAI-style images endpoint: point base_url at them (for xAI, https://api.x.ai).

Status: verified with gpt-image-1-mini (generated, re-hosted, downloaded, webhook retried and delivered), and with xAI's grok-imagine-image through this adapter.

ElevenLabs

{ "name": "elevenlabs", "kind": "elevenlabs", "base_url": "https://api.elevenlabs.io", "api_key_env": "ELEVENLABS_API_KEY" }

One upstream, five products. Which one a target uses is its "api" setting ("speech" when it is left out), never guessed from a model name:

[
  { "model": "speech", "cache_ttl_s": 2592000, "targets": [
    { "upstream": "elevenlabs", "model": "eleven_flash_v2_5", "price": { "per_input_char_usd": 0.0 } } ] },
  { "model": "sound", "targets": [
    { "upstream": "elevenlabs", "model": "eleven_text_to_sound_v2", "api": "sound", "default_seconds": 5, "max_seconds": 30, "price": { "per_second_usd": 0.0 } } ] },
  { "model": "music", "timeout_s": 600, "targets": [
    { "upstream": "elevenlabs", "model": "music_v1", "api": "music", "default_seconds": 30, "max_seconds": 120, "price": { "per_second_usd": 0.0 } } ] },
  { "model": "image", "targets": [
    { "upstream": "elevenlabs", "model": "gemini-2.5-flash-image", "api": "image", "price": { "per_output_usd": 0.0 } } ] },
  { "model": "video", "timeout_s": 900, "targets": [
    { "upstream": "elevenlabs", "model": "bytedance-seedance-v2-mini", "api": "video", "max_seconds": 10,
      "price": { "variants": [ { "when": { "resolution": "480p" }, "per_second_usd": 0.0 }, { "when": { "resolution": "720p" }, "per_second_usd": 0.0 } ] } } ] }
]

(The zeros are placeholders: take the prices from your ElevenLabs plan.)

The target's model is sent as model_id. Job input by product:

"api" Call input
speech one POST /v1/text-to-speech/{voice_id} text (required), voice_id (required; letters, digits, _ and -), output_format (default mp3_44100_128); anything else is passed through in the body.
sound one POST /v1/sound-generation text (required: what the effect is), duration (seconds, 0.5 to 30; the provider picks a length without it), loop, prompt_influence, output_format (default mp3_44100_128). Nothing else is forwarded.
music one POST /v1/music prompt, or composition_plan (ElevenLabs' own structure), never both; with a prompt, duration (seconds, 3 to 600); seed, force_instrumental, finetune_id, respect_sections_durations, sign_with_c2pa, output_format (the provider's choice without it). Nothing else is forwarded; in particular not store_for_inpainting, which keeps the song at the provider.
image POST /v1/flows/image, then GET /v1/flows/image/{id} until it ends prompt (required) and whatever the model takes (aspect_ratio, resolution, images, seed, ...), passed through.
video POST /v1/flows/video, then GET /v1/flows/video/{id} until it ends prompt (required), duration (whole seconds), and whatever the model takes (resolution, aspect_ratio, generate_audio, start_frame, ...), passed through.
curl http://127.0.0.1:8080/v1/jobs -H "Authorization: Bearer <gateway key>" -d '{"model":"speech","input":{"text":"Hello from omnigate.","voice_id":"<voice id>"}}'
curl http://127.0.0.1:8080/v1/jobs -H "Authorization: Bearer <gateway key>" -d '{"model":"sound","input":{"text":"a door closing softly","duration":2}}'

Speech is a good fit for cache_ttl_s: an identical request then reuses the stored audio with no provider call and no charge.

Status, by product:

Product Status
speech Verified with eleven_flash_v2_5: real MP3 generated, re-hosted and downloaded; an identical request served from cache; an unknown voice failed with the provider's reason.
sound Verified on 11 November 2026 with one job (eleven_text_to_sound_v2, 0.5 s): real MPEG audio (8,820 bytes), re-hosted, hash recorded, billed for the half second asked for. Looping, other formats and failures have only met the mock.
music Verified on 11 November 2026 with one job (music_v1, a prompt, 3 s, instrumental): real MPEG audio (48,945 bytes), re-hosted, billed for 3 s. A composition plan, longer pieces and a plan's refusal have only met the mock.
image, video Mock only for a successful job. The test workspace is on the Creator plan, and both endpoints answer 402 paid_plan_required below Pro. That refusal was run through the gateway on 11 November 2026: the job failed cleanly with the provider's message, unbilled; with a second target it moved on; a video length the model does not take came back as a validation error naming the field. Create, polling, the signed link and the download are written from ElevenLabs' documentation and have run only against tools/mock_media.py.

Gemini image and speech

Gemini's image and speech models are served only by Google's native API, so they use a media adapter instead of the text upstream.

{ "name": "gemini-media", "kind": "gemini", "base_url": "https://generativelanguage.googleapis.com", "api_key_env": "GEMINI_API_KEY" }
{
  "model": "gemini-image",
  "targets": [
    { "upstream": "gemini-media", "model": "<gemini image model id>", "price": { "per_output_usd": 0.0 } }
  ]
}
{
  "model": "gemini-speech",
  "cache_ttl_s": 86400,
  "targets": [
    { "upstream": "gemini-media", "model": "<gemini tts model id>", "price": { "per_input_char_usd": 0.0 } }
  ]
}

One synchronous POST /v1beta/models/{model}:generateContent. Job input is either the short form:

Field Meaning
prompt (or text) The prompt, or the text to speak.
voice For speech: a voice name such as Kore. Its presence also asks for audio output.

or Gemini's own request fields, sent through untouched: contents (a list) with optional generationConfig, systemInstruction and safetySettings. Use this for anything the short form does not cover, such as reference images or several speakers.

curl http://127.0.0.1:8080/v1/jobs -H "Authorization: Bearer <gateway key>" -d '{"model":"gemini-image","input":{"prompt":"a green cube"}}'
curl http://127.0.0.1:8080/v1/jobs -H "Authorization: Bearer <gateway key>" -d '{"model":"gemini-speech","input":{"text":"Hello from omnigate.","voice":"Kore"}}'

Files come back inline. Raw PCM speech (audio/L16) is wrapped in a WAV header so the stored file plays as it is. A prompt Google refuses fails the attempt with Google's reason (no_output: prompt blocked (...)).

Status: verified with gemini-3.1-flash-lite-image and gemini-3.8-flash-lite-tts: an image and a spoken sentence generated, stored and downloaded from the gateway; the repeat speech request served from cache; an unknown model failed with Google's reason.

Gemini video (Interactions API)

Gemini's video-capable "omni" models are reachable only through the Interactions API.

{ "name": "gemini-interactions", "kind": "gemini_interactions", "base_url": "https://generativelanguage.googleapis.com", "api_key_env": "GEMINI_API_KEY" }
{
  "model": "gemini-video",
  "timeout_s": 600,
  "targets": [
    { "upstream": "gemini-interactions", "model": "<gemini omni model id>", "price": { "per_job_usd": 0.0 } }
  ]
}

POST /v1beta/interactions. Job input is either prompt (or text), or a native Interactions body that has an input field; in both cases model is filled in from the target.

curl http://127.0.0.1:8080/v1/jobs -H "Authorization: Bearer <gateway key>" -d '{"model":"gemini-video","input":{"prompt":"a short clip of waves"}}'

A native body with "background": true makes Google answer before the work is done; the gateway then polls GET /v1beta/interactions/{id}:

curl http://127.0.0.1:8080/v1/jobs -H "Authorization: Bearer <gateway key>" -d '{"model":"gemini-video","input":{"input":"a longer clip of waves","background":true}}'

A foreground request is one HTTP call that lasts as long as the generation; timeout_s is its limit.

Status: verified with gemini-omni-1.2-flash: a short clip generated in one call, stored and downloaded from the gateway as an MP4, webhook delivered. Not verified: background interactions (the polling path has only met the mock).

xAI video

{ "name": "xai-video", "kind": "xai_video", "base_url": "https://api.x.ai", "api_key_env": "XAI_API_KEY" }
{
  "model": "grok-video",
  "timeout_s": 900,
  "targets": [
    { "upstream": "xai-video", "model": "<xai video model id>", "price": { "per_second_usd": 0.0 }, "default_seconds": 5, "min_seconds": 1, "max_seconds": 10 }
  ]
}

POST /v1/videos/generations with the job's input plus model, then GET /v1/videos/{request_id} until the status is done, failed or expired. input takes prompt and optionally duration (seconds), aspect_ratio, resolution and image: {"url": ...}.

curl http://127.0.0.1:8080/v1/jobs -H "Authorization: Bearer <gateway key>" -d '{"model":"grok-video","input":{"prompt":"a wave breaking","duration":4}}'

Price it with per_second_usd, which is multiplied by input.duration (or input.seconds, whichever is larger). With a price above 0:

None of the three settings is sent to xAI: set them to what the model does.

Status: verified with grok-imagine-video-1.5-lite: a one-second clip submitted, polled to completion, copied off xAI and downloaded from the gateway as an MP4, billed per second, webhook delivered; an unknown model failed with xAI's reason. Not verified: an input image, and the failed and expired states.

Sume

Sume is an aggregator: one key reaches image and video models of many makers through two generate endpoints, and every request is a job that is polled.

{ "name": "sume", "kind": "sume", "api_key_env": "SUME_API_KEY" }

base_url may be left out for this kind; it defaults to https://api.sume.com.

{
  "model": "veo-fast",
  "timeout_s": 900,
  "targets": [
    {
      "upstream": "sume",
      "model": "<sume video model id>",
      "router": "video",
      "max_seconds": 8,
      "price": {
        "variants": [
          { "when": { "resolution": "720p", "generate_audio": false }, "per_second_usd": 0.0 },
          { "when": { "resolution": "720p", "generate_audio": true }, "per_second_usd": 0.0 }
        ]
      }
    }
  ]
},
{
  "model": "sume-image",
  "targets": [
    { "upstream": "sume", "model": "<sume image model id>", "router": "image", "price": { "per_output_usd": 0.0 } }
  ]
}

Status: partly verified. The adapter was written from Sume's public documentation (read on 11 November 2026). Checked against the real service on 11 November 2026 with one image job (gpt-image-2, low quality) and one 3-second video job (gemini-omni-flash-1.2, 360p): submit, polling, the result and the download worked as written, and the video was billed from the duration Sume reported (3.029 s for a 3 s request). Cancel, refusals such as insufficient funds, idempotent replay and failed jobs have only met the mock.

Failover across media providers

List several targets in one media route; they may be of different kinds, as long as each accepts the same input:

{
  "model": "image",
  "timeout_s": 300,
  "targets": [
    { "upstream": "fal", "model": "<fal model id, e.g. owner/model>", "price": { "per_output_usd": 0.0 } },
    { "upstream": "replicate", "model": "<owner/name or owner/name:version>", "price": { "per_output_usd": 0.0 } }
  ]
}

The job's cost is the price of the target that delivered. Provider charges for failed attempts are not recorded or reconciled.

Not reachable

This page on GitHub