Skip to main content

Reaching an OpenAI-compatible gateway

A provider block names a gateway, the models it offers and where its credential lives. An entry keyed amazon-bedrock names an AWS account instead, which is reached by signing rather than by a token; everything below is about the other entries. The models a gateway ends up with are offered in /model beside Brave's roster and any AWS tiers. It takes nothing away from those rosters and does not move the default: what answers when nobody has chosen stays what it was, and the conversation budget stays where it was too.

{
"provider": {
"openrouter": {
"name": "OpenRouter",
"env": ["OPENROUTER_API_KEY"],
"models": {
"z-ai/glm-4.6": { "limit": { "context": 200000, "output": 8192 } }
}
}
}
}
WhereFieldWhat it holds
the key under providerthe gateway's id, which is also what a picker row names it by
the entrynamesomething friendlier to show than the id
envvariable names that may hold the bearer token, tried in order
modelsthe models to offer, keyed by the name the gateway knows each by
optionsbaseURLwhere requests go
apiKeya token written into the file directly
a modellimit.contextthat model's context window, in prompt tokens
optionsanything extra to put in the request body

The block is opencode's, field for field, so one copied out of opencode.json works unedited. Nothing is required that opencode does not require, and a field bravebot does not know is read past rather than refused. opencode's cost, modality and package fields do nothing here.

Where the requests go​

A gateway bravebot already knows an endpoint for needs no baseURL. openrouter is the name it knows today. Any other id needs one written down, and an entry with neither a known name nor a stated endpoint configures no service. A stated baseURL always wins, so a known name stays usable against a proxy or a private deployment.

The names it knows are compiled in, and nothing is fetched to resolve one. This value is where a bearer credential gets sent, so a service that could decide it could redirect your token by answering a request.

Google Vertex AI​

Vertex AI has an OpenAI-compatible endpoint that takes an API key. An entry keyed google-vertex reaches it:

{
"provider": {
"google-vertex": {
"env": ["GOOGLE_API_KEY"],
"options": { "project": "<your project id>", "location": "global" }
}
},
"model": "google-vertex/google/gemini-2.5-flash"
}

The host is built from project and location, which is global where you state none, and a project is required. One holding a character a project id cannot hold, such as / or @, configures nothing rather than sending your key somewhere else. A stated baseURL still wins.

The key is read from the variable env names and sent in the x-goog-api-key header, which is the only place Google reads it from. Export it as GOOGLE_API_KEY=<your key>, or name another variable in env. A block naming no env and no options.apiKey sends no key, and Vertex AI refuses it.

With no block, exporting GOOGLE_API_KEY and GOOGLE_CLOUD_PROJECT is enough, and VERTEX_LOCATION sets a location other than global. Both of the first two are needed, and a block replaces this. The service is only asked once a model named google-vertex/... is chosen. GOOGLE_API_KEY is a name other Google tools read as well, so the key you exported for one of them is what is sent here once you choose such a model.

Vertex AI has no model listing a key can call, so /model offers a short list of Gemini models built into bravebot, which bravebot doctor names. A block that lists models is offered those instead. No preview model is on the built-in list, because Google withdraws previews without notice. The list is what the global location serves, and another location may not serve all of it. A model on the built-in list is named with the id in front, as google-vertex/google/gemini-2.5-pro, in --model or the model key, and one off it is named the same way, as google-vertex/google/gemini-3-flash-preview. Signing in with Google Cloud credentials instead of a key is not supported.

The credential​

Name a variable in env and keep the token wherever you already keep secrets. Or store it with bravebot:

bravebot auth login gateway openrouter

asks for the key with nothing shown as you type it, and keeps it in ~/.bravebot/gateway-keys.json, readable only by your account, under the block's id. Signing in has the details. options.apiKey is read too, because it is opencode's field. Where more than one is present a variable wins, then the stored key, then options.apiKey. A long-lived token in a settings file is a token in a file people paste into issues.

A variable is read at the point a request needs it rather than once at startup, so exporting a new one takes effect in a session already open. A stored key is read when a session starts, so one stored while a session is open reaches the next one. A block that names somewhere for a credential to live and finds nothing there is a stale or missing token, and its requests are refused with the remedy named rather than sent. bravebot doctor says whether a credential was found, and whether it was a stored one, and never what it was. It prints an ends line for the block too, naming the host the token is presented to: that host is the only surface that revokes it, and deleting the value from this file, unsetting the variable or running bravebot auth logout gateway ends this machine's custody and leaves the token live there.

A block naming no credential at all is a different statement, and a supported one. No env, no options.apiKey and no stored key is you saying this gateway wants none: its requests carry no authorization header and its roster is asked for without one. doctor reports it as needing none rather than as missing one. Deciding this by endpoint instead would refuse the same local service reached across a LAN or through a reverse proxy, and a dummy apiKey would just teach people to write fake credentials into a file they paste into issues.

A local Ollama, or another gateway that wants no key​

Ollama wants no API key, so its block names none:

{
"provider": {
"ollama": {
"name": "Ollama (local)",
"options": { "baseURL": "http://localhost:11434/v1" }
}
},
"model": "ollama/qwen3-coder:30b"
}

baseURL is written down because ollama is not one of the names an endpoint is compiled in for. There is no models key, so Ollama is asked what it has pulled and /model lists what came back. A first run with nothing configured offers to write this block for you where Ollama is running (Importing from Claude Code, opencode or Ollama).

Which models are offered​

A block that lists models is taken at its word, in the order you wrote them, and costs no round trip. That is what keeps a configured gateway working with no network, and is the way to pin a short list out of a service offering hundreds.

A block that lists none has the gateway asked, except on Google Vertex AI, which has no listing to ask. That is the ordinary case rather than a mistake: opencode resolves its roster from a registry it fetches, so the commonest block copied out of it names a credential and nothing else. What your credential may reach is asked for first, and the service's full catalogue answers only where a gateway does not offer the narrower question. Models that cannot call tools are left out.

Nothing is capped. Ordering does that work instead: the model a session would use comes first and the rest are sorted by name. A listing that cannot be fetched contributes nothing and takes nothing away from the rest of the roster.

Naming one​

Where one model is reachable through more than one service, put the gateway's id in front of the name to say which you mean:

openrouter/z-ai/glm-4.6

The name is split once, at the first slash, because most gateway names contain one. The id picks the service and only the remainder is sent, the id being bravebot's own filing that no gateway has heard of. A bare name your block lists still finds its gateway, so a choice already recorded by /model keeps working.

The context window​

limit.context is optional. A model that states none is assumed to have 131,072 prompt tokens, the same deliberately low figure a Bedrock tier gets and for the same reason: a budget above the real window does not compact a conversation late, it stops compacting it at all. A window a gateway reports is taken where the file stated none; a figure in the file outranks it. Following opencode, limit needs output alongside context or it is not a limit and its figure is not read. output states how far a reply may run.

What a model's options can and cannot do​

Whatever you put there reaches the request body as it stands. Nothing parses it, knows what any of its fields mean, or validates them, so a misspelled routing field is a request the gateway rejects, or worse one it silently routes somewhere you did not intend.

It cannot replace what the turn itself built. The settings file names a destination, not what was asked.