routing

package
v0.1.0 Latest Latest
Warning

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

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

Documentation

Overview

Package routing parses route selector strings and expands them into ordered attempt candidates.

Grammar (v1): model-only, backend:model, backend.instance:model, failover (a|b|c), weighted ([weight=N]a^[weight=M]b), [first] on weighted branches, parallel (a!b!c) with per-leg [handicap=N] and [ttft_timeout=N], and ?query parameters. Parallel and weighted operators cannot be mixed in the same arm; failover (|) of parallel groups is allowed (a!b|c!d). The planner applies exclusions, optional health filtering, session first-request rules, and deterministic weighted selection via an injected RNG.

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrContinuationCandidatePinned = errors.New("routing: continuation candidate is pinned")
	ErrContinuationNativeMismatch  = errors.New("routing: continuation native lineage mismatch")
)
View Source
var ErrInvalidSelector = errors.New("routing: invalid route selector")

ErrInvalidSelector reports a syntactically invalid route selector.

View Source
var ErrNoEligibleCandidate = errors.New("routing: no eligible route candidate")

ErrNoEligibleCandidate means every candidate in the relevant scope was excluded or unhealthy.

View Source
var ErrUnknownBackend = errors.New("routing: unknown backend")

ErrUnknownBackend reports a compiled selector that names a backend identity not present in the current generation's side-effect-free registry view.

View Source
var ErrUnsafeExecutionComposition = errors.New("routing: unsafe backend execution composition")

ErrUnsafeExecutionComposition is the root sentinel for unsafe selector composition.

View Source
var ErrWeightedTotalTooLarge = errors.New("routing: weighted branch total too large")

ErrWeightedTotalTooLarge means the sum of effective branch weights exceeds math.MaxInt (cannot pass safely to the RNG) or overflowed int64 during accumulation.

View Source
var ErrWrongBackendCanonical = errors.New("routing: canonical model not registered for leaf backend")

ErrWrongBackendCanonical is returned when route planning binds a canonical that exists only on a different backend than the leaf's backend.

Functions

func ApplyModelOnlyBackends

func ApplyModelOnlyBackends(sel *Selector, defaultBackend string)

ApplyModelOnlyBackends sets Primary.Backend to defaultBackend wherever it is empty. defaultBackend must be non-empty; otherwise this is a no-op.

func ApplyNativeModelIDs deprecated

func ApplyNativeModelIDs(sel *Selector, resolver NativeModelResolver)

ApplyNativeModelIDs is retained as a deprecated alias that binds NativeModel without rewriting Model. Prefer BindNativeModelIDs; wrong-backend errors are dropped.

Deprecated: use BindNativeModelIDs and handle ErrWrongBackendCanonical.

func BackendIDsReferenced

func BackendIDsReferenced(sel *Selector) []string

BackendIDsReferenced returns every literal (non-model-only) backend ID embedded across sel's failover alternatives, weighted branches, and parallel branches, in selector order. Used to validate a candidate default route or alias replacement against the candidate backend set before publication (req 9.2). A nil selector returns nil.

func BindNativeModelIDs

func BindNativeModelIDs(sel *Selector, resolver NativeModelResolver) error

BindNativeModelIDs sets NativeModel on every selector leaf from the request-bound resolver without overwriting Primary.Model (logical/canonical identity). Wrong-backend canonical leaves return a typed error (fail-closed). Unknown raw/native leaves keep NativeModel empty (compatibility pass-through). Params, weights, handicaps, TTFT, affinity, and selector identity are untouched.

func CandidateEqual

func CandidateEqual(a, b AttemptCandidate) bool

CandidateEqual reports whether two AttemptCandidates have identical routing identity, including backend, model, native model, isParallel, handicap, markedFirst, role, and selector key.

func CandidateSetsEqual

func CandidateSetsEqual(a, b []AttemptCandidate) bool

CandidateSetsEqual reports whether two candidate slices match in exact order and membership.

func CheckContinuationCandidate

func CheckContinuationCandidate(constraint ContinuationConstraint, candidate AttemptCandidate) error

CheckContinuationCandidate rejects a candidate that cannot safely replay a provider-bound record. Canonical requirements remain available to the normal admission check; this check only enforces exact lineage/pinning.

func ComposeInitialCandidates

func ComposeInitialCandidates(
	raw string,
	aliases *AliasResolver,
	defaultBackend string,
	classes BackendExecutionResolver,
	policy config.ExecutionCompositionPolicy,
	resolver NativeModelResolver,
) ([]AttemptCandidate, *Selector, error)

ComposeInitialCandidates runs the full canonical routing composition sequence: 1. Selector compile (trim, alias resolution, parse, model-only defaulting, and unresolved-model-only rejection). 2. Execution composition validation under the active generation policy. 3. Request-bound native model binding (fail-closed on wrong-backend canonical). 4. Initial candidate extraction in exact order and membership.

It is pure and side-effect-free: no B-legs, no I/O, no live store mutation.

func DefaultBackendFromRouteSelector

func DefaultBackendFromRouteSelector(defaultRoute string) (string, error)

DefaultBackendFromRouteSelector extracts the backend id from a configured default route (for example "openai-responses:gpt-4o-mini"). The route must contain at least one primary with a non-empty backend (not model-only).

func FilterRoutePrefixes

func FilterRoutePrefixes(prefixes []string) []string

FilterRoutePrefixes trims, drops invalid (empty, colon- or slash-bearing), dedups, and sorts backend route-selector prefixes. Shared by runtime bundle composition and frontend PrefixSet construction so the validation rule lives in one place. A prefix is the "<prefix>:" segment of a route selector; it must not itself contain ":" (which would make it a full selector) or "/" (which collides with provider-namespace model syntax).

func IsDirectPrimary

func IsDirectPrimary(sel *Selector) bool

IsDirectPrimary returns true if the selector contains exactly one failover alternative and that alternative is a single direct Primary (not weighted or parallel).

func RejectUnknownBackends

func RejectUnknownBackends(sel *Selector, known map[string]struct{}) error

RejectUnknownBackends reports referenced backend IDs absent from known. A nil known set means identities were not available without I/O; the check is skipped. An empty (non-nil) set rejects every explicit backend ID.

func SelectorHasEmptyBackend

func SelectorHasEmptyBackend(sel *Selector) bool

SelectorHasEmptyBackend reports whether any primary still has an empty backend id.

func SelectorHasRequestSizeConstraints

func SelectorHasRequestSizeConstraints(sel *Selector) bool

SelectorHasRequestSizeConstraints reports whether any leaf has min/max context filters.

func ValidateExecutionComposition

func ValidateExecutionComposition(
	sel *Selector,
	classes BackendExecutionResolver,
	policy config.ExecutionCompositionPolicy,
) error

ValidateExecutionComposition checks whether a compiled selector AST conforms to the generation's execution composition policy. Under safe policy:

  • A direct primary is always permitted (regardless of backend execution class).
  • Composed selectors (weighted, parallel, thinker, failover) require every reachable configured backend to be explicitly classified as inference.
  • Reachable backends that are absent from the resolver are ignored here (preserving existing missing-backend authority).
  • If classes resolver is nil when evaluating a composite under safe policy, it fails closed.

Under unrestricted policy: - All compositions are allowed.

func ValidateModelAliases

func ValidateModelAliases(rules []ModelAliasRule) error

func ValidateModelAliasesConfig

func ValidateModelAliasesConfig(cfg *config.Config) error

ValidateModelAliasesConfig compiles model_aliases from cfg (regexp patterns and static replacement selectors). Call after config.LoadFile or equivalent decode; kept out of the config package to avoid a config<->routing import cycle.

func WithNativeModelResolver

func WithNativeModelResolver(ctx context.Context, resolver NativeModelResolver) context.Context

WithNativeModelResolver attaches resolver for request-scoped route planning.

Types

type AffinityMode

type AffinityMode string

AffinityMode controls route-wide backend stickiness. It is intentionally global selector metadata because affinity constrains the whole planning process.

const (
	AffinityNone    AffinityMode = ""
	AffinitySession AffinityMode = "session"
	AffinityClient  AffinityMode = "client"
)

type AliasResolver

type AliasResolver struct {
	// contains filtered or unexported fields
}

func NewAliasResolver

func NewAliasResolver(rules []ModelAliasRule) (*AliasResolver, error)

func (*AliasResolver) Resolve

func (r *AliasResolver) Resolve(s string) string

type AttemptCandidate

type AttemptCandidate struct {
	Primary Primary
	// Key matches Primary.String() for exclusion maps.
	Key string
	// MarkedFirst is true when this candidate came from a [first]-annotated weighted branch.
	MarkedFirst bool
	// IsParallel is true when this candidate belongs to a parallel group (selector '!' arm).
	IsParallel bool
	// Handicap is the per-leg head-start delay for parallel routing (0 = no handicap).
	Handicap time.Duration
	// InterleavedRole labels thinker/executor candidates in thinker-aware weighted cycles.
	// It is RoleNone for non-thinker weighted selectors.
	InterleavedRole interleavedstate.Role
	// SelectorKey is the thinker-aware weighted selector key the candidate was planned from.
	// Empty for non-thinker weighted selectors.
	SelectorKey string
}

AttemptCandidate is one concrete backend:model attempt after parsing and resolution.

func BackendFacingCandidate

func BackendFacingCandidate(c AttemptCandidate) AttemptCandidate

BackendFacingCandidate returns a copy with Primary.Model projected to the bound NativeID for true backend-dependent seams (Open, ResolveCaps, transport caps). Logical Model, Key, affinity, and other identity fields are preserved on the original candidate; only the returned copy's Model is rewritten.

func ExpandFailover

func ExpandFailover(sel *Selector, opt PlanOptions) ([]AttemptCandidate, error)

ExpandFailover resolves a parsed selector into an ordered list of candidates for the first pass: one entry per failover arm (left-to-right), skipping primary arms that are excluded/unhealthy, and resolving each weighted arm with pickWeighted. Arms with no eligible candidates are skipped. When a [first] branch is chosen, Session.FirstRequestConsumed is set (if Session is non-nil).

func InitialCandidates

func InitialCandidates(sel *Selector) []AttemptCandidate

InitialCandidates extracts all possible attempt candidates from a compiled selector AST, preserving exact sequential/fallback, weighted, and race candidate order and membership (Requirements 7.3, 8.3, 8.4, 10.3, 10.4). A nil selector or one with no alternatives returns nil.

func ReplanWeighted

func ReplanWeighted(w *Weighted, opt PlanOptions) (AttemptCandidate, error)

ReplanWeighted picks again within the same weighted group after excluding failed candidates. Req 7.4: retry/failover path ignores [first] and only considers remaining eligible weighted candidates.

type AttemptGroup

type AttemptGroup struct {
	Candidates []AttemptCandidate
	// NextThinkerCycle is the advanced thinker-aware weighted cycle cursor produced by
	// picking a thinker-aware weighted branch. It is nil for non-thinker weighted selectors
	// and for primary/parallel arms. Callers persist it when route selection becomes
	// authoritative so the next request in the same session continues from the next position.
	NextThinkerCycle *interleavedstate.CycleState
}

AttemptGroup is one eligible failover arm after planning. Single-candidate arms (primary/weighted) contain exactly one candidate; parallel arms contain one or more legs.

func ExpandFailoverGroups

func ExpandFailoverGroups(sel *Selector, opt PlanOptions) ([]AttemptGroup, error)

ExpandFailoverGroups resolves a parsed selector into ordered candidate groups (one group per eligible arm). Primary/weighted arms produce one candidate group of length 1; parallel arms produce one group with all eligible parallel legs.

type BackendExecutionResolver

type BackendExecutionResolver interface {
	ResolveBackendExecution(backendID string) (class lipsdk.BackendExecutionClass, ok bool)
}

BackendExecutionResolver resolves the execution class of a configured backend instance. If the backend instance is not configured in the active generation, ok is false.

type BackendExecutionResolverFunc

type BackendExecutionResolverFunc func(backendID string) (lipsdk.BackendExecutionClass, bool)

BackendExecutionResolverFunc allows using an ordinary function as a BackendExecutionResolver.

func (BackendExecutionResolverFunc) ResolveBackendExecution

func (f BackendExecutionResolverFunc) ResolveBackendExecution(backendID string) (lipsdk.BackendExecutionClass, bool)

type CandidateAdmissionCheck

type CandidateAdmissionCheck struct {
	Call              lipapi.Call
	Invocation        lipapi.Invocation
	Candidate         AttemptCandidate
	BackendCaps       lipapi.BackendCaps
	TransportCaps     lipapi.BackendTransportCaps
	ReplaySupport     lipapi.ReasoningReplaySupport
	DialectSupport    lipapi.DialectSupport
	ProjectionTarget  lipapi.LegacyProjectionTarget
	TransportPolicy   lipapi.TransportFallbackPolicy
	RequireProjection bool
	Continuation      *ContinuationConstraint
}

CandidateAdmissionCheck evaluates whether one planned candidate can represent the call before upstream work.

func (CandidateAdmissionCheck) Evaluate

Evaluate runs the canonical admission sequence for one candidate.

type ContinuationConstraint

type ContinuationConstraint struct {
	Lineage            lipcont.Lineage
	Requirements       lipapi.ProtocolRequirements
	NativeRequirements []lipcont.NativeRequirement
}

ContinuationConstraint is the routing projection of stored continuation lineage. Portable records leave BackendID/Model empty and may reroute.

type FailoverAlt

type FailoverAlt struct {
	Primary  *Primary
	Weighted *Weighted
	Parallel *Parallel
}

FailoverAlt is one arm of a failover selector (before the next '|'). Exactly one of Primary, Weighted, or Parallel is non-nil.

type GenerationSelectorValidator

type GenerationSelectorValidator struct {
	Aliases                    *AliasResolver
	DefaultBackend             string
	KnownBackends              map[string]struct{}
	BackendExecutionResolver   BackendExecutionResolver
	ExecutionCompositionPolicy config.ExecutionCompositionPolicy
}

GenerationSelectorValidator validates route selectors against the current generation's aliases, default backend, known backends, and execution composition policy (Requirements 7, 8). It implements routeoverride.SelectorValidator.

func NewGenerationSelectorValidator

func NewGenerationSelectorValidator(
	aliases *AliasResolver,
	defaultBackend string,
	knownBackends map[string]struct{},
	execResolver BackendExecutionResolver,
	policy config.ExecutionCompositionPolicy,
) *GenerationSelectorValidator

NewGenerationSelectorValidator constructs a GenerationSelectorValidator.

func (*GenerationSelectorValidator) HasRequestSizeConstraints

func (v *GenerationSelectorValidator) HasRequestSizeConstraints(raw string) (bool, error)

HasRequestSizeConstraints reports whether the compiled and validated selector contains any context min/max request-size filters (Requirements 7, 8, 19). A nil validator fails closed and returns an error.

func (*GenerationSelectorValidator) LegalCandidateBackends

func (v *GenerationSelectorValidator) LegalCandidateBackends() []string

LegalCandidateBackends extracts all backend IDs that can legally be selected under this generation's validator. If KnownBackends is nil, returns nil (unbounded/unknown backends). If KnownBackends is non-nil, returns a sorted slice of all known backend IDs.

func (*GenerationSelectorValidator) ValidateSelector

func (v *GenerationSelectorValidator) ValidateSelector(_ context.Context, raw string) error

ValidateSelector compiles raw, verifies all referenced backends are known, and enforces the execution composition policy. A nil validator fails closed: it cannot prove any selector legal.

type ModelAliasRule

type ModelAliasRule struct {
	Pattern     string
	Replacement string
}

ModelAliasRule is one regexp-based rewrite rule for a route selector string.

func ModelAliasRulesFromConfig

func ModelAliasRulesFromConfig(cfg *config.Config) []ModelAliasRule

ModelAliasRulesFromConfig maps decoded config.Config.ModelAliases into ModelAliasRule slices for alias compilation and validation. It returns nil when cfg is nil or has no entries.

type ModelBinding

type ModelBinding struct {
	Kind   ModelBindingKind
	Native string // set for ExactCanonical and KnownNative
}

ModelBinding is the multi-state resolution result for one backend+model leaf.

type ModelBindingKind

type ModelBindingKind int

ModelBindingKind distinguishes request-scoped native resolution outcomes. Exact-backend resolution never selects another backend's row (req 9.10).

const (
	// ModelBindingUnspecified is the zero value; treat as unknown pass-through.
	ModelBindingUnspecified ModelBindingKind = iota
	// ModelBindingExactCanonical maps (backend, canonical) to that backend's NativeID.
	ModelBindingExactCanonical
	// ModelBindingKnownNative means model is already a NativeID published for that backend.
	ModelBindingKnownNative
	// ModelBindingUnknown means the raw/native input is unrecognized; preserve as pass-through.
	ModelBindingUnknown
	// ModelBindingWrongBackend means the canonical is registered only on another backend.
	ModelBindingWrongBackend
)

type NativeModelResolver

type NativeModelResolver interface {
	ResolveModelBinding(backendID, model string) ModelBinding
}

NativeModelResolver maps a route leaf's backend+model using a request-bound registry view. Implementations must be fail-closed across backends.

func NativeModelResolverFromContext

func NativeModelResolverFromContext(ctx context.Context) (NativeModelResolver, bool)

NativeModelResolverFromContext returns the request-bound native resolver when present.

type Parallel

type Parallel struct {
	Branches []ParallelBranch
}

Parallel is a set of branches executed concurrently; the first to produce a non-whitespace content delta wins. Branches are separated by '!' in the selector string.

type ParallelBranch

type ParallelBranch struct {
	Target   Primary
	Handicap time.Duration
}

ParallelBranch is one leg of a parallel group.

type PlanOptions

type PlanOptions struct {
	Excluded  map[string]struct{}
	Unhealthy map[string]struct{}
	// RequestSize is the current request size estimate used for per-leaf min/max context filters.
	// When unavailable, size filters fail open and do not exclude candidates.
	RequestSize RequestSizeEstimate
	Session     *SessionRoutingState
	// PreferredCandidateKeys hints expand order when keys are already eligible (design §12, advisory).
	PreferredCandidateKeys []string
	// StickyBackendID forces a currently bound backend instance ahead of normal weighted/failover selection
	// when at least one candidate for that backend is eligible in the current selector.
	StickyBackendID string
	// Rand supplies weighted-branch rolls. When nil, weighted selection uses a fixed-seeded
	// math/rand/v2 PCG stream (deterministic, not crypto-safe); inject for tests or concurrency-safe rolls.
	Rand        Rng
	IsRetryPath bool
	// ThinkerCycle is the persisted thinker-aware weighted cycle cursor for the current selector.
	// A zero-value (empty) state means no cycle has been established yet; the planner builds a
	// fresh cycle from the weighted branches and starts at position 0.
	ThinkerCycle interleavedstate.CycleState
	// SuppressThinker skips the thinker position during cycle advancement (continuation turns)
	// and returns deterministic no-eligible-route outcomes when no executor candidate remains.
	SuppressThinker bool
}

PlanOptions configures planning: exclusions, health, session first-request consumption, RNG, retry path.

type Primary

type Primary struct {
	// Backend is empty for model-only selectors. It may contain dots (e.g. openai.azure).
	Backend string
	// Model is the requested/logical identity (canonical or raw pass-through).
	// Route keys, affinity, catalog matching, traces, and diagnostics use Model.
	Model string
	// NativeModel is the request-bound backend wire id when the registry mapped
	// this leaf. Empty means unresolved pass-through (use Model at seams).
	// NativeModel must not appear in String()/exclusion keys.
	NativeModel string
	Params      url.Values
	Size        RequestSizeConstraint
	TTFTTimeout *time.Duration
}

Primary is a concrete backend:model (or model-only) with optional query parameters.

func (Primary) String

func (p Primary) String() string

String returns a stable diagnostic key for a primary (exclusion / health maps).

func (Primary) TrimmedParam

func (p Primary) TrimmedParam(key string) string

TrimmedParam returns the first query parameter value with surrounding whitespace removed.

func (Primary) WireModel

func (p Primary) WireModel() string

WireModel returns the backend-facing model id: bound NativeModel when set, else Model.

type RequestSizeConstraint

type RequestSizeConstraint struct {
	MinContextTokens *int64
	MaxContextTokens *int64
}

RequestSizeConstraint carries per-leaf request token eligibility bounds.

type RequestSizeEstimate

type RequestSizeEstimate struct {
	Available bool
	Tokens    int64
	Basis     string
}

RequestSizeEstimate is a provider-neutral request token estimate for route planning.

type Rng

type Rng interface {
	Intn(n int) int
}

Rng abstracts a uniform integer RNG for weighted picks (see NewSeededRng, WrapRandV2).

func NewSeededRng

func NewSeededRng(seed int64) Rng

NewSeededRng returns a deterministic, non-crypto-strength RNG for weighted routing. The same seed yields the same stream (PCG with a zero second word).

func WrapRandV2

func WrapRandV2(r *randv2.Rand) Rng

WrapRandV2 wraps a *rand/v2.Rand as Rng (for example a single shared instance in tests).

type RoutingScenarioSpec

type RoutingScenarioSpec struct {
	ID               string
	InvariantSummary string
	TestName         string // exists in internal/core/routing/*_test.go
}

RoutingScenarioSpec links a stable scenario identifier to route-selector and planning invariants and the primary regression test (specification bundle).

func SpecBundleRoutingScenarios

func SpecBundleRoutingScenarios() []RoutingScenarioSpec

SpecBundleRoutingScenarios lists selector, alias, and planner invariants. Keep aligned with .kiro/steering/routing-and-orchestration.md and the referenced tests.

type Selector

type Selector struct {
	Alternatives      []FailoverAlt
	GlobalTTFTTimeout *time.Duration
	Affinity          AffinityMode
}

Selector is the root AST: an ordered failover chain (left-to-right).

func CompileSelector

func CompileSelector(raw string, aliases *AliasResolver, defaultBackend string) (*Selector, error)

CompileSelector is the shared pure compile path for admin preflight and buildRoutePlan: trim, alias resolution, parse, model-only defaulting, and unresolved-model-only rejection. It must not allocate B-legs, open backends, bind native models, or mutate affinity/weighted-first/interleaved state.

func Parse

func Parse(s string) (*Selector, error)

Parse parses a route selector string into an AST.

func PrepareSelector

func PrepareSelector(
	raw string,
	aliases *AliasResolver,
	defaultBackend string,
	classes BackendExecutionResolver,
	policy config.ExecutionCompositionPolicy,
	resolver NativeModelResolver,
) (*Selector, error)

PrepareSelector compiles the selector, validates execution composition under the active policy, and binds native model IDs if a resolver is configured. It is the shared pure preparation logic for both canonical route planning (Executor.buildRoutePlan) and wire assessment (ComposeInitialCandidates, InitialRouteAssessmentGate) to eliminate any risk of routing drift (Requirements 7, 8, 19).

type SessionRoutingState

type SessionRoutingState struct {
	FirstRequestConsumed bool
}

SessionRoutingState holds session-scoped routing flags (mirrors B2BUA A-leg state; persisted by caller in v1).

type UnsafeExecutionCompositionError

type UnsafeExecutionCompositionError struct {
	Composition string
	BackendID   string
	Class       lipsdk.BackendExecutionClass
	Policy      config.ExecutionCompositionPolicy
}

UnsafeExecutionCompositionError records details of an unsafe composition rejection.

func (*UnsafeExecutionCompositionError) Error

func (*UnsafeExecutionCompositionError) Unwrap

type Weighted

type Weighted struct {
	Branches []WeightedBranch
}

Weighted is a set of branches; exactly one is chosen per planning step using weights.

type WeightedBranch

type WeightedBranch struct {
	Weight int
	// IsFirst marks a [first] annotation (only meaningful when not on retry path and session allows).
	IsFirst bool
	// IsThinker marks a [thinker] annotation; the branch produces a planning memo
	// in interleaved-thinking cycles. At most one thinker branch is allowed per
	// weighted group, and it cannot combine with [first] on the same branch.
	IsThinker bool
	// Target is the resolved primary for this branch. Zero when Parallel is set.
	Target Primary
	// Parallel, when non-nil, marks this branch as targeting an embedded parallel
	// executor group. Allowed only on the non-thinker branch of a thinker hybrid
	// weighted selector; the parser enforces that narrow shape. Target is zero in
	// that case. General weighted/parallel mixing remains rejected.
	Parallel *Parallel
}

WeightedBranch is one weighted arm after splitting on '^'.

type WrongBackendCanonicalError

type WrongBackendCanonicalError struct {
	Backend string
	Model   string
}

WrongBackendCanonicalError names the rejected leaf for diagnostics.

func (*WrongBackendCanonicalError) Error

func (*WrongBackendCanonicalError) Unwrap

func (e *WrongBackendCanonicalError) Unwrap() error

Jump to

Keyboard shortcuts

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