Skip to main content

What virtual models do

A virtual model is a model you define, instead of one a provider advertises. GoModel picks the kind from whether you give it a target:
  • A redirect (an alias) is a stable name that points to a concrete provider model. Your app sends the name in the model field, and GoModel resolves it before sending the request upstream.
  • An access policy has no target. It gates an existing model selector by user_path — or disables it entirely — without adding a new name. See User Path for access control.
Create and manage both in the admin dashboard at Models -> New virtual model. Fill Target model to make a redirect; leave it empty to make an access policy on the Source selector. GoModel does not persist empty, ineffective access policies, whether saved from the dashboard or through the admin API. If a saved policy has no target, user paths, description, or slowdown and its enabled state matches the inherited/default access state, GoModel removes the stored row. It retains an otherwise empty policy when that row still overrides a disabled default or an inherited user-path restriction.

Add artificial model latency

Set Slowdown on a concrete model or redirect to add a fraction of measured inference time. Active values range from 0.1 to 10; for example, 0.5 adds 50%, so a response that takes 2 seconds upstream is returned after about 3 seconds. Set 0 to explicitly disable slowdown, including slowdown inherited from a concrete model. Leaving the field empty leaves the setting unspecified. Delays honor request cancellation. For Chat Completions and Responses API SSE streams, GoModel drains the upstream in the background and releases each read chunk on the scaled timeline. This preserves the provider’s relative chunk timing while delayed chunks accumulate in memory. A large factor on a long or high-volume stream can therefore use substantial memory. Realtime WebSocket sessions are not slowed. When an alias and its concrete target both have a slowdown, the alias value wins. An alias without its own value inherits the selected target model’s slowdown; set the alias to 0 to override that inheritance. This also applies to load-balanced virtual models after a target is selected. The rest of this page covers redirects.

Use stable names

You can expose names like regular and smarter instead of provider-specific model IDs.
  • regular -> anthropic/claude-sonnet-4-6
  • smarter -> anthropic/claude-opus-4-6
Your app can then send:

Load balance across models

A redirect can point at several target models and spread requests across them. Add more than one target in the editor (Add target) and pick a strategy:
  • Round-robin (default) rotates requests evenly across the targets. Set a per-target weight to bias the share — a target with weight 2 receives twice the traffic of a target with weight 1.
  • Lowest cost routes every request to the cheapest currently-available target, ranked by the model registry’s input + output per-token price. When a target has no registry pricing it is skipped while a priced target exists; if none are priced, the first listed target is used.
  • Failover always routes to the first currently-available target in the order listed: the targets form a priority list, and lower entries serve only while every entry above them is unavailable or fails. Weights and session keeping do not apply.
  • Adaptive (GoModel Pro) picks the target with the best observed latency, error rate, and price, backs off rate-limited or failing targets, and keeps a session on the provider whose prompt cache is warm. See Intelligent Routing. On an open-source build the strategy falls back to round-robin.
  • Plugin (strategy: plugin) delegates the choice to a loaded routing strategy plugin named by strategy_plugin, configured per redirect through strategy_config. The built-in cheapest_healthy prefers the cheapest (or fastest) target whose recent error rate is acceptable. Weights do not apply; if the plugin fails or is missing, the redirect falls back to weighted round-robin.
Targets that the gateway cannot currently serve (unknown model, provider down) are skipped automatically, so a redirect keeps working as long as one target is available. A redirect with a single target behaves exactly like a plain alias. Whatever the strategy, the remaining targets are the redirect’s failover chain: when the chosen target fails with a retryable error, the request is retried against the other available targets in the order listed. The editor’s Failover checkbox (on by default; failover: false as code) switches that off for one redirect, so a failed request is answered with the error instead; it can also be switched off globally or by the request’s workflow. See Failover for the errors that trigger a retry and for adding a failover chain to a real model by shadowing it. Requests belonging to one detected client session stick to the target that served the session first (Session keeping, on by default), so conversations keep their provider prompt cache warm across turns. Untick the editor’s Session keeping checkbox (or set session_affinity: false) to restore stateless balancing — see Session Keeping.
For example, point smart at both openai/gpt-4o and anthropic/claude-sonnet-4-6 with the round-robin strategy to split load, or at a premium and a budget model with the cost strategy to always take the cheaper one.

Reorder targets

The target list is an ordered list, and the editor keeps it editable: drag the grip handle (☰) at the left edge of each target row onto another row to change its position. The drop row is not replaced — the dragged row moves into its position and the rows in between shift. Keyboard users can focus the handle and press ArrowUp / ArrowDown for the same move. The handle shows only when the redirect has more than one target, and every strategy benefits: for failover the order is the priority chain (position 1 is tried first), for the balancing strategies position 1 is the first slot in the queue. Weights and explicit provider pins stay attached to their target when it moves.

Chain virtual models

A redirect target can name another virtual model instead of a concrete model. The inner virtual model is resolved at request time, so its targets, weights, and strategy are defined once and reused everywhere it is referenced — no duplicated target lists that drift apart.
Resolution composes level by level: the outer strategy picks one of its legs (a chained leg counts like any target, honoring its weight), and a chosen virtual-model leg then applies its own strategy among its own targets. Session keeping pins at every level, so a session stays on the same concrete model.
  • The cost strategy prices a chained leg at the cheapest concrete model currently available behind it.
  • A chained virtual model that is disabled, or has no available target, is skipped like an unavailable provider model.
  • Only the outer virtual model’s user_paths and slowdown apply to a request; the inner one’s scope is not consulted when it is reached through a chain.
  • /v1/models lists a chained redirect with the metadata of its first available concrete model.
A target written as one name is matched by name wherever it is declared — config.yaml, the dashboard, or the admin API — so team/cheap reaches the virtual model of that name even though it looks like provider/model. Set provider explicitly ({ provider: groq, model: llama-3.3-70b }) to pin a target to a concrete model regardless of any virtual model sharing the name. A redirect that shadows a real model and lists that model among its own targets (the shape the Models page creates when you add fallbacks or balancing to a model) covers the model rather than replacing it. An alias or any other redirect that names the model chains into that cover, so it gets the same balancing and failover a direct request gets — with one exception: the fallback reference of one such cover to another covered model reaches the model itself, not the other cover’s fallback list. That is what lets two models protect each other without forming a cycle, and keeps a fallback from sweeping the fallbacks of the fallback. Because a bare reference to a covered model names the model rather than the cover, the cover can be deleted while such references exist — they simply revert to the concrete model. A shadow that does not list its source replaces the model and is chained through like any other virtual model: references chain into its targets, and it cannot be deleted or renamed until they are repointed. Chains must be acyclic (a -> b -> a is rejected, with the cycle spelled out) and at most 8 virtual models deep. Both rules are enforced when saving from the dashboard and at startup for declarative entries. A target must exist when it is saved — create the inner virtual model first — and a virtual model that others chain through cannot be deleted or renamed until they are repointed.

Rename or repoint a redirect

Open a redirect in the dashboard and edit its Source to rename it. GoModel moves the definition to the new name in one step — the old name stops resolving, so update any app still sending it. Renaming onto a name that already belongs to another virtual model is rejected; renaming onto a concrete model name shadows that model (see Shadow a model). Redirects that shadow a model can be edited the same way — use the model row’s Edit redirect action. Virtual models declared as code keep their source fixed in configuration; rename them by editing the declaration.

Define virtual models as code

Operators who manage configuration as infrastructure-as-code can declare virtual models in config.yaml or the VIRTUAL_MODELS environment variable instead of the dashboard. Declarative entries are applied at startup, override any admin-store row with the same source, and are shown read-only in the dashboard (a Config badge). Edit the configuration to change them. In config.yaml:
The same list as a single environment variable (JSON), which overrides config.yaml entries with a matching source:
Each entry accepts source, a single target (shorthand) or a targets list, strategy (round_robin, cost, or failover), session_affinity (default true; see Session Keeping), failover (default true; see Failover), user_paths, description, slowdown (0 disables inherited slowdown; active factors range from 0.1 to 10), and enabled. A target model may name another declared virtual model (see Chain virtual models). Leave the targets empty to declare an access policy on the source selector. An invalid declaration (unknown strategy, missing or self-referential target, a chain cycle or a chain deeper than 8 virtual models, or a target provider that matches no configured provider — a typo) fails startup with a clear error listing the registered provider names. Two lenient exceptions keep valid setups booting:
  • A target provider that is declared under providers: but did not register — typically because its credentials are unset in this environment — only logs a warning; the target stays unavailable until the credentials resolve. A config.yaml shared across environments still boots everywhere.
  • Whether a target model exists is never checked at startup: the model catalog loads asynchronously, so availability is a runtime concern and an unavailable target is simply skipped at resolve time.

Expose only redirects

To hide provider models from GET /v1/models, set:
When this is enabled, GoModel returns enabled redirects from /v1/models instead of the full provider model list. The setting is also listed in .env.template.

List bare model IDs

By default GET /v1/models returns provider-qualified IDs such as openai/gpt-5. Some clients validate the model you configure against that list and only know plain names, so they never match. To list bare IDs instead, set:
The owned_by field still carries the provider name. Requests for a bare model ID keep working the same way in both modes. Caveat: when two providers expose the same model ID (for example gpt-5 on both openai and azure), only the provider an unqualified request routes to (the first registered one) is listed. To pin a name to a specific provider, add a virtual model with that name as its source:
The redirect wins over the provider entry in both the model list and routing.

Shadow a model

A redirect can “shadow” a model by using the same name as an existing model and pointing it somewhere else. This lets you override a requested model without changing application code. For example:
  • source: anthropic/claude-opus-4-6
  • target model: anthropic/claude-sonnet-4-6
Your app can keep sending anthropic/claude-opus-4-6, while GoModel routes the request to anthropic/claude-sonnet-4-6.

A/B testing

Redirects are useful for short model experiments. Move a redirect from one target to another, then compare app behavior, latency, and usage. For example, point smarter at Opus for one test and Sonnet for another. You can also shadow opus-4-6 with sonnet-4-6 to check whether the same app flow still works.
Last modified on September 4, 2026