Skip to content

OpenRouter plugin

The openrouter plugin gives Genkit access to OpenRouter, a gateway that serves models from many vendors behind one OpenAI-compatible endpoint. Models are named under the openrouter/ provider prefix.

Terminal window
go get github.com/firebase/genkit/go

Add &openrouter.OpenRouter{} to your plugin list. The plugin reads the API key from the OPENROUTER_API_KEY environment variable.

package main
import (
"context"
"fmt"
"log"
"github.com/firebase/genkit/go/ai"
"github.com/firebase/genkit/go/genkit"
"github.com/firebase/genkit/go/plugins/compat_oai/openrouter"
)
func main() {
ctx := context.Background()
g := genkit.Init(ctx,
genkit.WithPlugins(&openrouter.OpenRouter{}),
genkit.WithDefaultModel("openrouter/anthropic/claude-sonnet-4.5"),
)
text, err := genkit.GenerateText(ctx, g,
ai.WithPrompt("Explain reinforcement learning in two sentences."))
if err != nil {
log.Fatalf("could not generate: %v", err)
}
fmt.Println(text)
}

You must provide an API key from OpenRouter. You can get one from your OpenRouter account settings. Set OPENROUTER_API_KEY, or set the APIKey field. SiteURL and AppName are attribution only: they ride as the HTTP-Referer and X-Title headers, which name your application on OpenRouter’s public rankings and change nothing else about a request. Extra OpenAI client request options ride in Opts, applied after the plugin defaults so they win on overlap.

import (
"context"
"os"
"github.com/firebase/genkit/go/genkit"
"github.com/firebase/genkit/go/plugins/compat_oai/openrouter"
"github.com/openai/openai-go/option"
)
ctx := context.Background()
g := genkit.Init(ctx, genkit.WithPlugins(&openrouter.OpenRouter{
APIKey: os.Getenv("MY_OPENROUTER_KEY"),
SiteURL: "https://example.com",
AppName: "My Genkit app",
Opts: []option.RequestOption{
option.WithBaseURL("https://openrouter.ai/api/v1"),
},
}))

genkit.Init panics when neither the APIKey field nor OPENROUTER_API_KEY is set. The endpoint defaults to https://openrouter.ai/api/v1; override it with the OPENROUTER_BASE_URL environment variable or with option.WithBaseURL in Opts.

As always, avoid embedding API keys directly in your code.

An OpenRouter model ID already carries the upstream vendor’s prefix, and Genkit adds its own, so a full model name has two slashes:

ai.WithModelName("openrouter/openai/gpt-5")
ai.WithModelName("openrouter/anthropic/claude-sonnet-4.5")
ai.WithModelName("openrouter/meta-llama/llama-4-70b-instruct:free")

The first segment is this plugin’s provider prefix, and the rest is the ID OpenRouter serves. The ID passed to openrouter.ModelRef works either way: ModelRef("anthropic/claude-sonnet-4.5", nil) and ModelRef("openrouter/anthropic/claude-sonnet-4.5", nil) name the same model.

OpenRouter’s variant suffixes work as part of the ID. :free picks the no-cost tier of a model, :nitro the fastest provider serving it, and :floor the cheapest.

The plugin registers no models when it initializes, and it lists no catalog, so the Dev UI shows no browsable model list for OpenRouter. Two reasons: an action descriptor carries a full copy of the request and response schemas, so a descriptor per catalog entry would put megabytes on every reflection poll; and OpenRouter fronts hundreds of models from dozens of vendors and adds more weekly, so any curated list would be stale.

What this costs you is discovery: there is no list to pick a model from, so take the ID from OpenRouter’s model list and paste it in. What you get in return is that every ID the gateway serves works by name, including a model released after your Genkit version.

Every model the plugin resolves is described with the same deliberately permissive capabilities: multiturn, tools, tool choice, system role, and media. The two ways to be wrong are not symmetric. A capability declared too narrow is refused by Genkit before the request is sent, which blocks a model that would have worked, while one declared too wide reaches OpenRouter, which answers with the real reason. Constrained generation is the exception, left unclaimed on purpose: a large share of the catalog lacks it natively, and unset, an output schema reaches the model as prompt instructions, which every model handles and which returns the same typed result.

openrouter.ModelRef pairs a model ID with a typed openrouter.ChatConfig, so the config is checked where you write it and validated against the model’s schema before the request goes out.

resp, err := genkit.Generate(ctx, g,
ai.WithModel(openrouter.ModelRef("openai/gpt-5", &openrouter.ChatConfig{
Provider: &openrouter.ProviderRouting{
Sort: openrouter.ProviderSortPrice,
DataCollection: openrouter.DataCollectionDeny,
},
Models: []string{"anthropic/claude-haiku-4.5"},
})),
ai.WithPrompt("Share a joke about bananas."),
)
if err != nil {
log.Fatalf("could not generate: %v", err)
}
fmt.Println(resp.Text())

You can also name a model as a string with ai.WithModelName or genkit.WithDefaultModel, and pass the config separately with ai.WithConfig(&openrouter.ChatConfig{...}). The OpenRouter sample runs this as a streaming flow you can call from the Dev UI.

The same model is served by several providers at different prices, speeds, and data policies. ChatConfig.Provider chooses among them, and is the control the gateway exists for.

capPerMillion := 3.0
resp, err := genkit.Generate(ctx, g,
ai.WithModel(openrouter.ModelRef("meta-llama/llama-4-70b-instruct", &openrouter.ChatConfig{
Provider: &openrouter.ProviderRouting{
Only: []string{"together", "fireworks"},
Quantizations: []string{"fp8", "fp16"},
MaxPrice: &openrouter.MaxPrice{Completion: &capPerMillion},
},
})),
ai.WithPrompt("Summarize the plot of Hamlet in three sentences."),
)
if err != nil {
log.Fatalf("could not generate: %v", err)
}
fmt.Println(resp.Text())
FieldTypeNotes
Order[]stringProvider slugs to try in order before any fallback.
Only[]stringRestricts routing to these provider slugs.
Ignore[]stringSkips these provider slugs.
AllowFallbacks*boolDefaults to true. false fails the request rather than letting another provider serve it.
RequireParameters*boolRoutes only to providers that honor every parameter the request carries.
DataCollectionopenrouter.DataCollectionallow, the default, or deny to restrict routing to providers that do not store request data.
ZDR*boolRestricts routing to zero data retention endpoints.
Sortopenrouter.ProviderSortprice, throughput, or latency, instead of the default load balancing.
Quantizations[]stringQuantization levels a provider must serve the model at, such as int4, fp8, or bf16. Not a closed set, since OpenRouter adds levels as hardware gains them.
MaxPrice*openrouter.MaxPriceCaps Prompt and Completion in USD per million tokens, and Request and Image in USD each. A request no provider can serve within the cap fails rather than falling back to a dearer one.
PreferredMinThroughput*float64Deprioritizes providers below this many output tokens per second. They stay eligible as a fallback.
PreferredMaxLatency*float64Deprioritizes providers slower than this many seconds to first token. They stay eligible as a fallback.

Sort, PreferredMinThroughput, and PreferredMaxLatency also have an object form, a partition or per-percentile thresholds, that this struct does not declare. Reach it through the Extra passthrough, which replaces the whole provider object because a colliding key wins over the field it collides with.

&openrouter.ChatConfig{
RequestConfig: compat_oai.RequestConfig{
Extra: map[string]any{
"provider": map[string]any{
"sort": map[string]any{"by": "price", "partition": "model"},
},
},
},
}

ChatConfig.Models lists further models to try, in order, when the requested one is unavailable, rate-limited, or refuses. The model the request names is tried first, and the gateway does the switch, so it costs no extra round trip.

&openrouter.ChatConfig{
Models: []string{"anthropic/claude-haiku-4.5", "google/gemini-2.5-flash"},
}

This is OpenRouter’s own fallback, distinct from the Fallback middleware, which cascades across Genkit model references and works with any provider.

ChatConfig.Reasoning controls the thinking a model does before it answers. OpenRouter normalizes every vendor’s reasoning controls onto one shape, so the same config reaches an OpenAI, an Anthropic, and a Gemini model. Reasoning arrives as a Genkit reasoning part; read it with resp.Reasoning().

resp, err := genkit.Generate(ctx, g,
ai.WithModel(openrouter.ModelRef("deepseek/deepseek-r1", &openrouter.ChatConfig{
Reasoning: &openrouter.ReasoningConfig{Effort: openrouter.ReasoningEffortHigh},
})),
ai.WithPrompt("Work through this step by step: what is heavier, a kilo of steel or a kilo of feathers?"),
)
if err != nil {
log.Fatalf("could not generate: %v", err)
}
fmt.Println(resp.Reasoning())
fmt.Println(resp.Text())

Effort is one of none, minimal, low, medium, high, xhigh, or max. Which levels a model takes is the model’s to decide: a level the upstream vendor does not offer is an error from OpenRouter rather than from the plugin, and none is rejected by a model that always reasons. MaxTokens sets an exact token budget instead and overrides Effort; vendors that take a budget usually reject one under 1,024 tokens, a per-vendor limit rather than a documented API-wide one. Exclude keeps the reasoning out of the response without stopping the model from doing it, and Enabled turns reasoning on with the vendor’s defaults, which OpenRouter treats as medium effort. Effort and MaxTokens imply it. A ReasoningConfig with no field set sends nothing, so a config built conditionally never enables reasoning by accident.

openrouter.ChatConfig carries the sampling fields OpenRouter normalizes across vendors plus the gateway controls:

FieldTypeNotes
Temperature*float64Randomness of token selection, 0 to 2.
TopP*float64Nucleus sampling threshold, 0 to 1.
TopK*intLimits sampling to the K most likely tokens. 0, the default, applies no limit. Some vendors ignore it.
MaxOutputTokensintSent as the API’s max_tokens. See the caution above before setting it on a reasoning model.
StopSequences[]stringUp to four.
FrequencyPenalty*float64-2 to 2.
PresencePenalty*float64-2 to 2.
RepetitionPenalty*float640 to 2, where 1 is neutral. Penalizes tokens by whether they appeared in the input.
MinP*float640 to 1. Minimum probability a token needs relative to the most likely one.
TopA*float640 to 1. Filters tokens by a threshold scaled from the most likely token’s probability.
Seed*intMakes generation reproducible on a best-effort basis.
LogProbs*boolRequests log probabilities for the output tokens.
TopLogProbs*int0 to 20. Requires LogProbs.
ParallelToolCalls*boolfalse caps the model at one tool call per response.
UserstringIdentifies the end user a request is made for, which OpenRouter uses to isolate abuse to one user rather than the whole key.
Models[]stringFurther models to fall back to, in order.
Provider*openrouter.ProviderRoutingWhich upstream providers may serve the request.
Reasoning*openrouter.ReasoningConfigHow much the model thinks before it answers.
Plugins[]map[string]anyOpenRouter request plugins such as web search, each an object with an id and that plugin’s own options, such as {"id": "web", "max_results": 3}. Sent verbatim rather than typed, since the roster changes on OpenRouter’s schedule.
Transforms[]stringPrompt transforms to apply, currently middle-out, which compresses a prompt that would overflow the model’s context by dropping from the middle.
SessionIDstringGroups related requests so they keep reaching the same upstream provider, which is what keeps a multi-turn conversation on one provider’s prompt cache.
ServiceTieropenrouter.ServiceTierauto, default, fast, flex, priority, or scale.
Metadatamap[string]stringUp to 16 pairs attached to the request, readable later on OpenRouter’s activity pages. Keys up to 64 characters, values up to 512.

Pointer fields separate unset from a deliberate zero. Three documented request fields are deliberately absent: n asks for several completion choices and bills for all of them while Genkit reads only the first, and route and usage are deprecated by OpenRouter and have no effect.

ChatConfig also embeds compat_oai.RequestConfig, which every plugin in the family shares: a per-request APIKey, a Version pin, and an Extra map whose keys ride to the wire verbatim under OpenRouter’s own names. See the OpenAI-compatible plugin page.

Correcting what the plugin knows about a model

Section titled “Correcting what the plugin knows about a model”

Every model works without an entry in Models. Supply one to narrow a model whose real capabilities you know are tighter than the permissive defaults, so Genkit refuses the request locally instead of paying for the upstream rejection. Keys are the model ID, bare or provider-prefixed, and fields left at their zero value keep what the plugin resolved.

g := genkit.Init(ctx, genkit.WithPlugins(&openrouter.OpenRouter{
Models: map[string]ai.ModelOptions{
// A text-only model, so Genkit refuses media before the request is sent.
"mistralai/mistral-7b-instruct": {
Supports: &ai.ModelSupports{
Multiturn: true,
Tools: true,
SystemRole: true,
},
},
},
}))

The narrowing is enforced rather than advertised: generating with media against that model fails locally with an error naming the missing media support.

Reasoning text, streamed token usage, gateway cost, and mid-generation provider failures are handled the same way for every plugin in this family. See the OpenAI-compatible plugin page.

Cost is worth reading here in particular, since pricing every request is part of what a gateway does. OpenRouter reports it in USD as resp.Usage.Custom["cost"]. Presence of the key decides, not its value: a :free model is priced at an explicit zero, which is an answer, so read it with the two-value map form rather than testing for a value above zero.

if cost, ok := resp.Usage.Custom["cost"]; ok {
fmt.Printf("this request cost %v USD\n", cost)
}