openrouter

package
v1.12.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 17, 2026 License: Apache-2.0 Imports: 7 Imported by: 0

README

OpenRouter Plugin

This plugin provides Genkit support for OpenRouter, a gateway that serves models from many vendors behind one OpenAI-compatible endpoint.

Setup

Set an OpenRouter API key:

export OPENROUTER_API_KEY=<your-api-key>

The plugin uses https://openrouter.ai/api/v1 by default. Set OPENROUTER_BASE_URL, or pass option.WithBaseURL through the plugin's Opts, to use another compatible endpoint.

import (
    "context"

    "github.com/firebase/genkit/go/ai"
    "github.com/firebase/genkit/go/genkit"
    "github.com/firebase/genkit/go/plugins/compat_oai/openrouter"
)

ctx := context.Background()
g := genkit.Init(ctx,
    genkit.WithPlugins(&openrouter.OpenRouter{}),
    genkit.WithDefaultModel("openrouter/anthropic/claude-sonnet-4.5"),
)

response, err := genkit.Generate(ctx, g, ai.WithPrompt("Explain reinforcement learning."))

SiteURL and AppName are optional. They name the application on OpenRouter's public rankings, sent as the HTTP-Referer and X-Title headers, and change nothing else about a request.

Reasoning output is returned as Genkit reasoning parts and is available through response.Reasoning().

Models

The plugin registers no models and carries no catalog. OpenRouter serves hundreds of models and adds more weekly, so every model resolves on demand instead:

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

A model ID keeps its upstream vendor's prefix, so a Genkit action name carries two slashes: the first is this plugin's provider prefix and the rest is the ID OpenRouter serves. Variant suffixes work the same way, including :free, :nitro for the fastest provider, and :floor for the cheapest.

For the same reason, the plugin advertises nothing to the Dev UI's model list. Models stay usable by name; only the browsable catalog is absent.

Every resolved model is described permissively: multi-turn, tools, tool choice, system role, and media are all claimed. This is deliberate. 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 and comes back with the real reason. Native constrained output is the exception, left unclaimed so that structured output falls back to schema instructions in the prompt, which every model handles and which returns the same typed result.

Correct a model whose real capabilities are narrower through Models:

plugin := &openrouter.OpenRouter{Models: map[string]ai.ModelOptions{
    // A text-only model, so Genkit refuses media locally rather than paying
    // for the upstream rejection.
    "mistralai/mistral-7b-instruct": {Supports: &ai.ModelSupports{
        Multiturn: true, Tools: true, SystemRole: true,
    }},
}}

Fields left at their zero value keep what the plugin resolves, so an entry can pin one capability without restating the rest.

The current model list is at https://openrouter.ai/models, and the API reference is at https://openrouter.ai/docs.

Config

Models take a typed openrouter.ChatConfig: the generation fields OpenRouter normalizes across vendors, plus the gateway controls that are the reason to route through it. openrouter.ModelRef carries the config with the model ID:

response, 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-sonnet-4.5"},
        Reasoning: &openrouter.ReasoningConfig{Effort: openrouter.ReasoningEffortHigh},
    })),
    ai.WithPrompt("Work through this step by step."),
)
  • provider chooses which upstream providers may serve the request: order, only, ignore, sort by price, throughput, or latency, maxPrice, dataCollection, zdr, requireParameters, and quantizations.
  • models is a fallback chain, tried in order when the requested model is unavailable, rate-limited, or refuses.
  • reasoning sets effort or an exact maxTokens budget, with exclude to reason without returning the reasoning.
  • plugins enables OpenRouter's request plugins, such as web search. They are sent verbatim rather than typed, since the roster and each plugin's options change on OpenRouter's schedule: Plugins: []map[string]any{{"id": "web", "max_results": 3}}.
  • transforms, sessionId, serviceTier, and metadata cover context compression, sticky routing, scheduling, and request tagging.
  • topK, minP, topA, and repetitionPenalty are the sampling knobs the OpenAI schema has no home for.

maxOutputTokens reaches OpenRouter as max_tokens, which reasoning models spend on thinking before they emit anything visible. A budget that is comfortable for a plain model can be consumed entirely by reasoning, leaving an empty answer and a length finish reason. Leave it unset, or budget for the thinking as well, on any model that reasons.

Every config also carries the settings Genkit owns: version pins the exact model version a request is served by, apiKey (settable only from Go code) serves one request with a different credential, and extra forwards request body fields the config does not declare, keyed by OpenRouter's wire names.

extra is also the way to reach the request shapes this config does not declare, such as the object form of provider.sort or the per-percentile form of the throughput and latency preferences. An extra wins over the field it collides with, so it replaces the whole declared object:

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

Usage and failures

OpenRouter prices every request it routes and reports what it charged. The amount arrives as cost on the response's usage, in credits:

response.Usage.Custom["cost"]

It needs no request field. OpenRouter used to gate this behind usage: {include: true}, which is now deprecated and does nothing; the full accounting is returned either way, on streaming and non-streaming responses alike. A :free model reports an explicit cost of 0, which is kept: the key's presence says the request was priced.

A provider that fails part-way through a generation is a case worth handling. OpenRouter has already sent a 200 by then, so it cannot answer with an error status. It returns the text produced before the failure, a finish reason of other, and the reason in response.FinishMessage. Check the finish reason before trusting a short answer:

if response.FinishReason == ai.FinishReasonOther && response.FinishMessage != "" {
    // Upstream failed mid-generation. response.Text() is partial.
}

The error object also rides whole on the response metadata, as response.Raw.(map[string]any)["error"]. It carries what the message alone does not: the failure's code, and metadata naming the upstream provider that failed, which is what provider.ignore takes to route the retry around it.

A request that fails before any output returns an ordinary error instead, as does a stream that breaks part-way through.

Not supported

This plugin serves the chat completions endpoint only. OpenRouter's image and audio generation endpoints are not part of it.

Three request fields are left out on purpose. 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. Anything else undeclared still reaches the wire through extra.

Live tests

Live tests are skipped unless OPENROUTER_API_KEY is set:

go test -race ./plugins/compat_oai/openrouter -run '^TestPluginLive$' -v -count=1

They spend on ordinary catalog models named at the top of openrouter_live_test.go. Swap in whatever the key has credit for.

Documentation

Overview

Package openrouter provides a Genkit plugin for OpenRouter, a gateway that serves models from many providers behind one OpenAI-compatible endpoint.

OpenRouter is not a model vendor, so this plugin carries no model catalog. Every model resolves by name on demand, under an ID that keeps the upstream vendor's prefix:

ai.WithModelName("openrouter/anthropic/claude-sonnet-4.5")

A resolved model is described with permissive capabilities, which is what makes an arbitrary model usable without an entry per model. Correct a model whose real capabilities are narrower through OpenRouter.Models.

See https://openrouter.ai/docs.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ModelRef

func ModelRef(id string, config *ChatConfig) ai.ModelRef

ModelRef names a model and carries the config to generate with, so the config is typed at the call site instead of an any the model checks at runtime. A nil config leaves the request's config unset.

ai.WithModel(openrouter.ModelRef("anthropic/claude-sonnet-4.5", &openrouter.ChatConfig{
	Provider: &openrouter.ProviderRouting{Sort: openrouter.ProviderSortThroughput},
}))

id is the model ID, with or without this plugin's provider prefix. It keeps the upstream vendor's prefix either way.

Types

type ChatConfig

type ChatConfig struct {
	compat_oai.RequestConfig

	// Temperature controls the degree of randomness in token selection, from
	// 0 to 2.
	Temperature *float64 `` /* 153-byte string literal not displayed */
	// TopP is the nucleus sampling threshold, from 0 to 1.
	TopP *float64 `` /* 190-byte string literal not displayed */
	// TopK limits sampling to the K most likely tokens, sent as the API's
	// top_k. 0, the default, applies no limit. Some vendors ignore it.
	TopK *int `` /* 173-byte string literal not displayed */
	// MaxOutputTokens is the maximum number of tokens to generate, sent as
	// the API's max_tokens.
	MaxOutputTokens int `` /* 148-byte string literal not displayed */
	// StopSequences stop generation when produced by the model, up to four.
	StopSequences []string `` /* 145-byte string literal not displayed */
	// FrequencyPenalty penalizes tokens by their frequency so far, from -2.0
	// to 2.0.
	FrequencyPenalty *float64 `` /* 154-byte string literal not displayed */
	// PresencePenalty penalizes tokens that have appeared at all, from -2.0
	// to 2.0.
	PresencePenalty *float64 `` /* 153-byte string literal not displayed */
	// RepetitionPenalty penalizes tokens by whether they appeared in the
	// input, from 0 to 2, sent as the API's repetition_penalty. 1 is neutral.
	RepetitionPenalty *float64 `` /* 213-byte string literal not displayed */
	// MinP is the minimum probability a token needs relative to the most
	// likely one, from 0 to 1, sent as the API's min_p.
	MinP *float64 `` /* 184-byte string literal not displayed */
	// TopA filters tokens by a threshold scaled from the most likely token's
	// probability, from 0 to 1, sent as the API's top_a.
	TopA *float64 `` /* 196-byte string literal not displayed */
	// Seed makes generation reproducible across calls when set, on a
	// best-effort basis.
	Seed *int `json:"seed,omitempty" jsonschema_description:"Makes generation reproducible across calls when set, on a best-effort basis."`
	// LogProbs requests log probabilities for the output tokens.
	LogProbs *bool `json:"logProbs,omitempty" jsonschema_description:"Requests log probabilities for the output tokens."`
	// TopLogProbs is how many of the most likely tokens to return log
	// probabilities for at each position, from 0 to 20; it requires LogProbs.
	TopLogProbs *int `` /* 205-byte string literal not displayed */
	// ParallelToolCalls lets the model request several tool calls in one
	// response, which it may do by default. It applies to a request that
	// carries tools.
	ParallelToolCalls *bool `` /* 186-byte string literal not displayed */
	// User identifies the end user a request is made for, which OpenRouter
	// uses to isolate abuse to one user rather than the whole key.
	User string `` /* 171-byte string literal not displayed */

	// Models lists further models to fall back to, in order, when the
	// requested one is unavailable, rate-limited, or refuses. The model the
	// request names is tried first.
	Models []string `` /* 191-byte string literal not displayed */
	// Provider chooses which upstream providers may serve the request.
	Provider *ProviderRouting `json:"provider,omitempty" jsonschema_description:"Chooses which upstream providers may serve the request, and in what order."`
	// Reasoning controls the reasoning the model does before answering.
	Reasoning *ReasoningConfig `json:"reasoning,omitempty" jsonschema_description:"Controls the reasoning the model does before answering."`
	// Plugins enables OpenRouter's request plugins, such as web search and
	// file parsing, each an object with an id and that plugin's own options:
	//
	//	Plugins: []map[string]any{{"id": "web", "max_results": 3}}
	//
	// They are sent verbatim rather than typed: the roster and each plugin's
	// options change on OpenRouter's schedule, and a struct here would reject
	// an option the gateway accepts. See
	// https://openrouter.ai/docs/features/web-search.
	Plugins []map[string]any `` /* 214-byte string literal not displayed */
	// Transforms are the prompt transforms to apply, currently "middle-out",
	// which compresses a prompt that would overflow the model's context by
	// dropping from the middle.
	Transforms []string `` /* 197-byte string literal not displayed */
	// SessionID groups related requests so they keep reaching the same
	// upstream provider, sent as the API's session_id. It is what keeps a
	// multi-turn conversation on one provider's prompt cache.
	SessionID string `` /* 222-byte string literal not displayed */
	// ServiceTier selects how the request is scheduled and billed upstream.
	ServiceTier ServiceTier `` /* 231-byte string literal not displayed */
	// Metadata attaches key-value pairs to the request, readable later on
	// OpenRouter's activity pages. It takes up to 16 pairs, with keys up to
	// 64 characters and values up to 512.
	Metadata map[string]string `` /* 201-byte string literal not displayed */
}

ChatConfig is the per-request config for models served through OpenRouter: the sampling fields the gateway normalizes across vendors, plus the routing controls that are the reason to route through it at all. See https://openrouter.ai/docs/api-reference/chat-completion.

Several documented request fields are deliberately absent. n asks for several completion choices and bills for all of them, while Genkit reads only the first. route and usage are deprecated by OpenRouter and have no effect. cache_control is a message annotation rather than a request field, so it has no home in a config. Anything undeclared still reaches the wire through compat_oai.RequestConfig.Extra.

func (ChatConfig) ApplyToChatCompletion

func (c ChatConfig) ApplyToChatCompletion(params *openai.ChatCompletionNewParams)

ApplyToChatCompletion implements compat_oai.ChatConfig: the fields the OpenAI schema already has land on their chat completion counterparts, and the sampling knobs and routing controls OpenRouter adds ride as extra request fields.

type DataCollection

type DataCollection string

DataCollection is whether a request may reach a provider that may store it.

const (
	// DataCollectionAllow permits any provider, which is the default.
	DataCollectionAllow DataCollection = "allow"
	// DataCollectionDeny restricts routing to providers that do not store
	// request data.
	DataCollectionDeny DataCollection = "deny"
)

type MaxPrice

type MaxPrice struct {
	// Prompt is the cap on the input price, per million tokens.
	Prompt *float64 `json:"prompt,omitempty" jsonschema:"minimum=0" jsonschema_description:"Cap on the input price, in USD per million tokens."`
	// Completion is the cap on the output price, per million tokens.
	Completion *float64 `` /* 127-byte string literal not displayed */
	// Request is the cap on the per-request price.
	Request *float64 `json:"request,omitempty" jsonschema:"minimum=0" jsonschema_description:"Cap on the per-request price, in USD."`
	// Image is the cap on the per-image price.
	Image *float64 `json:"image,omitempty" jsonschema:"minimum=0" jsonschema_description:"Cap on the per-image price, in USD."`
}

MaxPrice caps what a request may cost, in USD per million tokens for the token fields. A request that no provider can serve within the cap fails rather than falling back to a dearer one.

type OpenRouter

type OpenRouter struct {
	// APIKey is the OpenRouter API key. If empty, OPENROUTER_API_KEY is
	// consulted.
	APIKey string
	// SiteURL identifies the calling application on OpenRouter's rankings,
	// sent as the HTTP-Referer header. It is optional and affects
	// attribution only.
	SiteURL string
	// AppName is the title the calling application appears under on
	// OpenRouter's rankings, sent as the X-Title header. It is optional and
	// affects attribution only.
	AppName string
	// Opts contains additional OpenAI client request options, such as
	// [option.WithBaseURL] for a different endpoint (OPENROUTER_BASE_URL
	// works too). Options supplied here are applied after the plugin
	// defaults, so they win on overlap.
	Opts []option.RequestOption

	// Models overrides what the plugin knows about a model, keyed by model
	// ID, bare or provider-prefixed. Every model already works without an
	// entry: OpenRouter serves hundreds of models from dozens of vendors and
	// adds more weekly, so the plugin curates no catalog and describes every
	// model it resolves with the same deliberately permissive capabilities.
	// 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 the model cannot serve
	// it. Constrained output is the exception, left unclaimed on purpose:
	// a large share of the catalog lacks it natively, and unset, Genkit
	// falls back to putting the schema in the prompt, which every model
	// handles and which returns the same typed result.
	//
	// Supply an entry to correct what a model can actually do:
	//
	//	&openrouter.OpenRouter{Models: map[string]ai.ModelOptions{
	//		// A text-only model, so Genkit refuses media locally rather
	//		// than paying for the upstream rejection.
	//		"mistralai/mistral-7b-instruct": {Supports: &ai.ModelSupports{
	//			Multiturn: true, Tools: true, SystemRole: true,
	//		}},
	//	}}
	//
	// Fields left at their zero value keep what the plugin resolves, so an
	// entry can pin one capability without restating the rest. The model ID
	// keeps the upstream vendor's prefix, so it contains a slash; the
	// optional provider prefix is this plugin's own, as in
	// "openrouter/mistralai/mistral-7b-instruct".
	Models map[string]ai.ModelOptions
	// contains filtered or unexported fields
}

OpenRouter configures the OpenRouter plugin.

func (*OpenRouter) Init

func (o *OpenRouter) Init(ctx context.Context) []api.Action

Init implements genkit.Plugin. It registers no models: OpenRouter's catalog is too large and too fast-moving to enumerate, so every model is resolved on demand by OpenRouter.ResolveAction.

func (*OpenRouter) ListActions

func (o *OpenRouter) ListActions(ctx context.Context) []api.ActionDesc

ListActions returns no descriptors. OpenRouter serves hundreds of models, and a descriptor carries a full copy of the request and response schemas, so listing the catalog would put megabytes on every reflection poll for a list nobody reads in full. Models stay reachable by name through OpenRouter.ResolveAction.

func (*OpenRouter) Name

func (o *OpenRouter) Name() string

Name implements genkit.Plugin.

func (*OpenRouter) ResolveAction

func (o *OpenRouter) ResolveAction(atype api.ActionType, id string) api.Action

ResolveAction dynamically builds a model served by OpenRouter, described by the plugin's config schema and capabilities. Any model ID the gateway serves resolves, whether or not this plugin has heard of it.

type ProviderRouting

type ProviderRouting struct {
	// Order lists provider slugs to try in order before any fallback.
	Order []string `` /* 127-byte string literal not displayed */
	// Only restricts routing to these provider slugs.
	Only []string `json:"only,omitempty" jsonschema_description:"Restricts routing to these provider slugs."`
	// Ignore skips these provider slugs.
	Ignore []string `json:"ignore,omitempty" jsonschema_description:"Skips these provider slugs."`
	// AllowFallbacks lets another provider serve the request when the chosen
	// ones fail. It defaults to true; false fails the request instead.
	AllowFallbacks *bool `` /* 174-byte string literal not displayed */
	// RequireParameters routes only to providers that honor every parameter
	// the request carries, rather than to one that would ignore some.
	RequireParameters *bool `` /* 175-byte string literal not displayed */
	// DataCollection restricts routing to providers that do not store request
	// data when set to deny.
	DataCollection DataCollection `` /* 189-byte string literal not displayed */
	// ZDR restricts routing to zero data retention endpoints.
	ZDR *bool `json:"zdr,omitempty" jsonschema_description:"Restricts routing to zero data retention endpoints."`
	// Sort ranks providers by one metric instead of OpenRouter's default load
	// balancing.
	Sort ProviderSort `` /* 187-byte string literal not displayed */
	// Quantizations restricts routing to providers serving the model at these
	// quantization levels, e.g. int4, int8, fp8, fp16, bf16, or fp32. The
	// set is not enumerated in the schema: OpenRouter adds levels as hardware
	// gains them, and a closed list here would reject a level it accepts.
	Quantizations []string `` /* 177-byte string literal not displayed */
	// MaxPrice caps what the request may cost.
	MaxPrice *MaxPrice `` /* 177-byte string literal not displayed */
	// PreferredMinThroughput deprioritizes providers below this many output
	// tokens per second. They stay eligible as a fallback.
	PreferredMinThroughput *float64 `` /* 187-byte string literal not displayed */
	// PreferredMaxLatency deprioritizes providers slower than this many
	// seconds to first token. They stay eligible as a fallback.
	PreferredMaxLatency *float64 `` /* 188-byte string literal not displayed */
}

ProviderRouting chooses which upstream providers may serve a request and in what order, sent as the API's provider object. This is the control the gateway exists for: the same model is served by several providers at different prices, speeds, and data policies.

Sort, PreferredMinThroughput, and PreferredMaxLatency also have an object form (a partition, or per-percentile thresholds) that this struct does not declare. Send the whole provider object through compat_oai.RequestConfig.Extra to reach it: an extra wins over the field it collides with, so the raw object replaces this one wholesale.

See https://openrouter.ai/docs/features/provider-routing.

type ProviderSort

type ProviderSort string

ProviderSort is the metric upstream providers are ranked by, replacing OpenRouter's default load balancing.

const (
	// ProviderSortPrice ranks the cheapest provider first.
	ProviderSortPrice ProviderSort = "price"
	// ProviderSortThroughput ranks the fastest provider first.
	ProviderSortThroughput ProviderSort = "throughput"
	// ProviderSortLatency ranks the provider with the lowest time to first
	// token first.
	ProviderSortLatency ProviderSort = "latency"
)

type ReasoningConfig

type ReasoningConfig struct {
	// Effort is how much reasoning to spend, from none to max. Vendors that
	// budget in tokens rather than levels take MaxTokens instead.
	Effort ReasoningEffort `` /* 220-byte string literal not displayed */
	// MaxTokens is the exact token budget to reason with, sent as the API's
	// max_tokens inside the reasoning object. It overrides Effort. Vendors
	// that take a budget usually reject one under 1,024 tokens, which is a
	// per-vendor limit rather than a documented API-wide one.
	MaxTokens int `` /* 199-byte string literal not displayed */
	// Exclude keeps the reasoning out of the response without stopping the
	// model from reasoning.
	Exclude *bool `` /* 132-byte string literal not displayed */
	// Enabled turns reasoning on with the vendor's own defaults, which
	// OpenRouter treats as medium effort. Effort and MaxTokens imply it.
	Enabled *bool `` /* 169-byte string literal not displayed */
}

ReasoningConfig controls the reasoning a model does before it answers, sent as the API's reasoning object. See https://openrouter.ai/docs/use-cases/reasoning-tokens.

type ReasoningEffort

type ReasoningEffort string

ReasoningEffort is how much reasoning a reasoning-capable model spends before it answers. OpenRouter normalizes the level across vendors, so the same value reaches an OpenAI, an Anthropic, and a Gemini model.

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 one from here.

const (
	// ReasoningEffortNone disables reasoning. Models that always reason
	// reject it.
	ReasoningEffortNone ReasoningEffort = "none"
	// ReasoningEffortMinimal is the shallowest level that still reasons.
	ReasoningEffortMinimal ReasoningEffort = "minimal"
	// ReasoningEffortLow is fast reasoning, for latency-sensitive work.
	ReasoningEffortLow ReasoningEffort = "low"
	// ReasoningEffortMedium is the level [ReasoningConfig.Enabled] selects on
	// its own.
	ReasoningEffortMedium ReasoningEffort = "medium"
	// ReasoningEffortHigh is deeper thinking, for hard multi-step problems.
	ReasoningEffortHigh ReasoningEffort = "high"
	// ReasoningEffortXHigh is deeper still, offered by a few vendors.
	ReasoningEffortXHigh ReasoningEffort = "xhigh"
	// ReasoningEffortMax is the deepest level any vendor offers.
	ReasoningEffortMax ReasoningEffort = "max"
)

type ServiceTier

type ServiceTier string

ServiceTier selects how a request is scheduled and billed upstream.

It is declared here rather than reused from openai.ChatCompletionNewParamsServiceTier because the sets differ: a config advertising the SDK's type would omit "fast" and "scale".

const (
	// ServiceTierAuto lets OpenRouter pick the tier.
	ServiceTierAuto ServiceTier = "auto"
	// ServiceTierDefault is standard scheduling and billing.
	ServiceTierDefault ServiceTier = "default"
	// ServiceTierFast buys faster scheduling.
	ServiceTierFast ServiceTier = "fast"
	// ServiceTierFlex trades latency for a lower rate.
	ServiceTierFlex ServiceTier = "flex"
	// ServiceTierPriority buys the fastest scheduling at the highest rate.
	ServiceTierPriority ServiceTier = "priority"
	// ServiceTierScale is the tier for sustained high volume.
	ServiceTierScale ServiceTier = "scale"
)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL