> ## Documentation index
> Fetch the complete documentation index at: https://rolter-ai-rolter.docs7.io/llms.txt
> Ask it with GET https://rolter-ai-rolter.docs7.io/ask?q=<keywords or question>. Short keywords rank best.
> Use these to discover all available pages before exploring further.

# The Rolter Dashboard

> How to sign in to the control-plane dashboard, what the left rail holds, how the org/team/project scope works, and how navigation adapts to a phone or a tablet.

The dashboard is the control plane's web UI. It is served by `rolter-control` on the same port as the API (`4001` by default) and works fully offline: it ships no runtime fonts, scripts or images from a CDN.

## Signing in

Open the control plane's address in a browser. You will see one of three things:

- **A password form** — the deployment has local accounts. Use the e-mail and password an administrator created for you, or the ones you chose when accepting an invitation link.
- **"Continue with …" buttons** — the organization registered an identity provider ([Single sign-on](/security/single-sign-on)). Password sign-in may be turned off alongside them.
- **An e-mail-only prompt** — the control plane is running in _open mode_ with no admin token and no accounts. Anything you enter opens the dashboard; there is no session to protect. This is meant for a first local run only.

<Note>
  A control plane started from `rolter.toml` alone, with `ROLTER_ADMIN_TOKEN` set but no `--database-url`, has no accounts to sign in to, so the sign-in card asks for the admin token instead of an e-mail address. The token is kept as the bearer every request carries, and it is what the request log, usage and provider health accept there, so the Dashboard, Logs and Health screens show data once it is entered. A missing or wrong token is explained on the screen that needed it, with a button back to the token prompt. The screens that need a database still report that they do.
</Note>

Your session is re-checked every time the dashboard loads. If it has expired, you are returned to the sign-in screen with a note saying so rather than a page full of failed requests.

The language switcher sits at the bottom of the rail, and on the sign-in card, so you can pick English or Russian before you have an account. It opens a small menu: the arrow keys move between the languages, Enter picks one, and Escape closes the menu and puts focus back on the button.

## Getting started

A fresh deployment opens the Dashboard on a **Getting started** card: the four steps between an empty control plane and a client calling Rolter, plus the request that client would send. That request is addressed to the base URL saved under [Client Settings](/configuration/client-settings#base-url), and shows a prompt to save one when none is saved or your role cannot read it. The dashboard's own `/gw` proxy requires a dashboard session, so it is never offered to a client outside the dashboard.

1. **Make a call with no credentials.** The gateway ships a built-in `fake-llm` model that answers without an upstream or a provider key, so the Playground works before anything is configured. You do not need a key of your own either: the Playground mints one for itself, and for a project with no routes yet that key reaches `fake-llm` and nothing else.
2. **Connect a provider.** The key is sealed in the store and never shown again.
3. **Add a route** — the model name your clients ask for, and where it lands.
4. **Mint a virtual key.** Clients authenticate with it rather than with your provider key, which is what makes per-key budgets and logs possible.

Every step runs inside a project. When the team selected in the scope switcher has no project yet, the card shows a **Create project** button that opens the same dialog as **New project** in the switcher's project menu, and selects the new project once it is created. With no team yet, the card asks you to add an organisation and a team in the switcher first. Creating a project takes the admin role, so for other roles the button is disabled and names the role it needs.

Each step is ticked from the deployment itself, not from a checklist kept in your browser: a step reads as done when the row exists, so work another administrator did shows up on reload. A step whose form your role may not open is shown as a disabled button naming the role it needs, rather than a link into a screen that would refuse you.

The card is for whoever sets the deployment up. If your role can add none of a provider, a route or a virtual key (a member or a viewer, typically), the Dashboard opens straight on its traffic panels and the card is not shown at all. The same happens when your role may not read the providers, routes or keys it would tick off. Neither case is reported as an error.

Once any step is done the card shrinks to one line, such as _2 of 4 · Next: Add a route_, with a primary button for the next undone step and **Show steps** to open the list again. Inside the list only that next step carries a primary button, and a finished step has none. While the deployment has traffic the card sits under the four figures rather than above them.

**Dismiss** hides the card and leaves a **Getting started** button in its place, so it is never gone for good. The card retires itself once a provider, a route and a virtual key all exist. Traffic plays no part in that, so a quiet day does not bring it back, and a deployment without ClickHouse retires it as well.

## The overview figures

Under the filters, one line says what the figures cover: whose requests, which window and how many filters. A superadmin, or a control plane in open mode, sees every request on the deployment; anyone else sees the requests their roles can read, and the line says that.

The Dashboard's first block is eight tiles, in the order an operator asks the questions: **Gateways**, **Providers**, **p95 latency**, **Error rate**, **Response-cache hit rate**, **Prompt-cache read share**, **Requests** and **Spend**. **Gateways** counts the live gateways and says how many are behind on config or have stopped answering; it reads the same fleet summary as the status pill in the header, so the two agree, and it stays on screen when the deployment has no ClickHouse. **Providers** counts the upstream providers that meet their SLA out of those that have reported health, names the worst one with its uptime (amber while some are below the SLA, red when none is up) and links to **Health**; it reads the same health rollup as that screen, narrowed to the providers your roles reach, and a deployment without ClickHouse sees a note in its place. The other six are figures of the selected window; if the summary read fails or is slow, **Gateways** stays and the others show their own placeholder or error beside it. **p95 latency** is the 95th percentile over the whole window, not a mean, since a mean hides the slow tail. **Response-cache hit rate** is the share of the window's requests that rolter's own response cache answered, with the count under it; a deployment with no response cache reads 0.0% and says none came from it. It does not count a provider's prompt caching: that is the **Prompt-cache read share**, the part of the window's prompt tokens a provider served from its own prompt cache (`cache_read_tokens` over `prompt_tokens`), which is what cache-aware balancing exists to raise. Only providers that report cached tokens [in a field Rolter reads](/observability/logs-and-cost#cached-prompts-and-thinking-tokens) can show a share; when none did in the window, the tile reads — and says so, instead of 0%. A gateway that is behind on config or has stopped answering turns the tile's line amber, the colour of the header pill; red is kept for no gateway being live. Each figure is compared with the window of the same length just before it (for **Month to date**, the same stretch of last month) and carries an arrow and the change, such as _+32% vs. the previous period_. The colour says what the move means rather than which way it went: more requests is good news, higher latency or a higher error rate is bad news, and spend rising or falling is neither, so it stays grey. A change of under 1% is drawn flat. When the window before had no traffic, as on a deployment's first day, there is nothing to compare with and no arrow is drawn. Under the error rate, the number of failed requests is shown too; with no earlier window to compare against it turns red once errors pass 1% of requests and stays grey below that.

The p95, the cache-hit rate and the error rate are measured over the window's requests, so a window with none has nothing to measure. Each reads **—** with _No requests in this window_ under them, instead of `0 ms` and `0.00 %`, which would read as a measurement of a quiet deployment. The request count and the spend stay real zeros. The line under the spend chart's title names the currency the deployment is configured in, the same one the amounts are written in.

Without ClickHouse the Dashboard has no traffic to count. It shows one note in place of the figures and charts, naming `CLICKHOUSE_URL` as the setting to add. The note is information about the deployment rather than an error, so it offers no retry and stays on screen until the setting changes. The setup checklist above it is unaffected.

"Last 24 hours" means the 24 hours before each read, not before you opened the page. The Dashboard reads again on its own, so a tab left open for an afternoon still reports a single day, and so does the refresh button in the header.

The window picker above the cards switches the figures and charts to another [reporting window](/observability/logs-and-cost#time-windows), kept in the address as `window=7d`. **Filters** next to it opens the same four filters as **LLM Logs**: **Model**, **Virtual key**, **Business unit** and **Customer**. They narrow the figures, the spend chart, the by-model table and **Recent requests** to the matching traffic, inside the requests your role can read. The address carries them as `model=`, `key=`, `business_unit=` and `customer=`, so a reload keeps them and a copied link opens the same view; **Clear filters** drops them all. When a filter matches nothing, the cards say no requests match it, not that no traffic has been served. **Saved views** next to it saves the window and the filters under a name and applies them again later; see [Saved views](/observability/logs-and-cost#saved-views). **Recent requests** always lists the latest requests of the last 24 hours, whatever window is picked.

When part of the window's spend was billed for responses a guardrail or plugin withheld, or successful requests reported no usage, a notice under the tiles says how many and how much, and links to LLM Logs narrowed to those rows (see [Withheld and unknown-usage traffic](/observability/logs-and-cost#withheld-and-unknown-usage-traffic)). Neither notice shows when its count is zero.

**By model** is one table: the eight busiest models by request count, busiest first, each with a colour, a bar for its share of the busiest model's requests, its p95 latency and its spend. A line under the table counts the models it does not list.

**Recent requests** lists the eight latest requests with their time, model, status and latency. The 24-hour window crosses midnight, so a request from another day shows its date under the clock; one from today shows the clock alone. Each row opens that request in [LLM Logs](/observability/logs-and-cost), and **Open in LLM Logs** under the table opens the whole log with the window and the filters the figures were read with. On a phone the four columns share the card's width without scrolling.

## The left rail

Everything lives under one of these groups. Screens you cannot read with your role are hidden; a deployment-wide screen is shown to a superadmin and to the [deployment role](/security/rbac#deployment-roles) that reaches it (an operator for the cluster, runtime policy, feature flags, alerting and connectors; a security auditor, read only, for the security, logging, compatibility and guardrail settings, the audit log and the config export), and hidden for everyone else.

| Group                                        | What is inside                                                                                                                                                                                                                                                      |
| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Playground**                               | Send chat, embedding, image, audio or realtime requests through the gateway with one of your virtual keys.                                                                                                                                                          |
| **Observability**                            | Dashboard (fleet health, p95 latency, error rate, cache hits, spend), LLM Logs, Savings, MCP Logs, Connectors, Logs Settings, Log Erasure.                                                                                                                          |
| **Models**                                   | Model Catalog, Providers, Provider Groups, Budgets & Limits, Routing Rules, Complexity Router, Provider Health, Model Prices, Model Settings.                                                                                                                       |
| **MCP Gateway**                              | MCP Catalog, Library, Tool Groups, Auth Sessions, OAuth Grants, MCP Settings.                                                                                                                                                                                       |
| **Plugins**                                  | Webhook plugins in the request/response pipeline.                                                                                                                                                                                                                   |
| **Alerting**                                 | Channels, Rules, History. See [Alerting](/observability/alerting).                                                                                                                                                                                                  |
| **Governance**                               | Virtual Keys, Users, Teams, Business Units, Customers, User Provisioning (SCIM), Single sign-on, LDAP, API Tokens, Roles & Permissions, Access Profiles, Audit Logs.                                                                                                |
| **Guardrails**                               | Rules and providers for content, PII and safety checks.                                                                                                                                                                                                             |
| **Cluster Config**                           | The gateways that poll this control plane. A gateway names itself from `ROLTER_NODE_ID`, else `HOSTNAME`, else the host's own name, and logs which one it used at boot. Set `ROLTER_NODE_ID` when several gateways share a host, or they all heartbeat as one node. |
| **Adaptive Routing**                         | Live scores from gateways on the adaptive strategy, and its settings.                                                                                                                                                                                               |
| **Prompt Repository**, **Skills Repository** | Versioned prompt templates and agent skills shared across the org.                                                                                                                                                                                                  |
| **Settings**                                 | Client settings, compatibility, the effective config viewer, security, your own virtual keys, performance tuning, feature flags.                                                                                                                                    |

The effective config viewer shows the configuration the control plane stores, from `rolter.toml` and its database, with credentials removed; any signed-in account can read it, and sees the providers, routes and groups of its own organizations and projects (a superadmin sees all). A provider, route or file virtual key the control plane leaves out of the gateway snapshot is marked **not served** on its row with the reason, and the lines from `GET /api/v1/config/problems` are listed above the tables. A superadmin also sees how many live gateways run the current config version, linked to **Cluster Config**. The **Every section** list below the tables has a filter that matches a section's name or anything written in it. The viewer also exports the deployment's configuration as an importable `rolter.toml` — see [Config Export](/configuration/config-export).

When the rail is folded to icons, select a group's icon (or press Enter, or the right or down arrow, on it) to open a small panel beside it listing the group's screens. Pick a screen to go there; Escape, a click elsewhere, or tabbing past the last screen closes the panel, and Escape puts focus back on the icon. The group that holds the screen you are on is marked with the same red thread the screen itself carries on the full rail. The panel lists only the screens your role may read, as the full rail does.

A name too long for the rail's width ends in an ellipsis; hold the pointer over it to read the whole name. That covers the signed-in name and role in the account card at the foot of the rail, and the "update available" note beside the version. Dragging the rail's right edge wider shows more of it.

The search box at the top of the rail filters the entries by name; press `/` from anywhere to jump into it. Matching a group's own name keeps everything filed under it, and a query that matches nothing says so rather than leaving the rail blank.

### Experimental entries

Some entries carry a small **Experimental** badge beside their name. That marks a subsystem which is usable but not yet settled: it may change shape, or be removed, in a minor release, and it is exempt from the compatibility promise the rest of the surface makes. Hover the badge to read what specifically is unfinished, in the language the dashboard is set to. When the rail is folded to icons the badge becomes a small dot on the entry's icon, and hovering the entry names it.

The list comes from the running control plane, not from the browser, so it is always the truth about the build you are using: an entry loses its badge as soon as you upgrade to a release where that subsystem has settled. Entries with no badge are stable.

## Keyboard shortcuts

| Key                                  | What it does                                                                                                                            |
| ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- |
| `⌘K` (`Ctrl-K` on Windows and Linux) | Opens the **command palette**.                                                                                                          |
| `/`                                  | Puts the caret in the rail's search box. Ignored while you are typing in a field, so a slash in a model name or a prompt stays a slash. |
| `?`                                  | Opens **Keyboard shortcuts**, the list of everything on this table. Ignored while you are typing, for the same reason `/` is.           |
| `Tab`, from the top of the page      | Reveals **Skip to content**, which jumps focus past the rail into the screen.                                                           |

You do not have to memorise the table: the palette prints `⌘K` beside its own
search field, the rail's search box prints `/`, and `?` shows the whole list.
Each shortcut is printed with the key your keyboard actually has — `⌘` on a
Mac, `Ctrl` everywhere else.

There is no keyboard required to find them either. The keyboard icon in the
rail's footer, beside the magnifier that opens the palette, opens the same
**Keyboard shortcuts** sheet, and names the key it stands in for when you hover
it. It is there at every width, including the drawer the rail becomes on a
phone.

Every dropdown and actions menu works the same way as the palette: open it and type, and the list narrows to what your letters spell in order, best match first, with the matching letters underlined. `oai` finds `openai`; a query that matches nothing says so. Arrow keys move through the matches and Enter picks the first. Pickers for a provider show its logo beside the name, and a provider without a logo gets its initial in a box.

The command palette searches two things at once. Screens first: every entry in
the rail, matched loosely, so `rr` finds Routing Rules and `audlog` finds Audit
Logs. Then records — virtual keys, providers and routes in the current scope,
by name — each labelled with what it is, opening the screen that lists it. With
nothing typed, the palette offers the screens you opened last.

Paste a request ID or a trace ID and, when nothing else matches it, the palette
offers **Open request … in LLM Logs**, which opens the log on that id. See
[Finding one request by its id](/observability/logs-and-cost#finding-one-request-by-its-id).

Arrow keys move the selection, Enter opens it, Escape closes the palette and
puts focus back where it was. The magnifier at the bottom of the rail opens the
same palette with the mouse. The palette only searches what your account may
read, and only the record lists the dashboard already loads — it is not a
full-text search over logs or requests. An id is looked up exactly, not searched.

## The scope switcher

Most screens show data for one **organization → team → project** chain. The switcher sits at the top of the rail, directly under the logo, and shows the chain you are on as `org / team / project`; a long name is shortened and the full path is its tooltip. Open it to change any of the three: each level has its own labelled picker, and picking one keeps the switcher open, since the level below it usually changes too. Press **Escape**, or click away, to close it.

A level you cannot choose yet is greyed out and says what it waits for: a team needs an organization, a project needs a team. A level with nothing in it says so, and its **⋯** button still offers to create the first one.

The **⋯** button at the end of each row holds what can be done to that level: **New organization**, **New team** or **New project**; **Project settings**; and the matching **Delete…**, which asks for confirmation and names what it removes. An entry your role may not use is greyed out and names the role it needs. Creating an organization is for superadmins.

The choice is remembered per browser; a fresh browser starts on the first organization you belong to. On the folded rail (and between `md` and `lg`, where the rail is always folded) the switcher is a building icon whose popover opens beside the rail; below `md` it is the same control inside the navigation drawer.

Screens that are deployment-wide (feature flags, security, performance, cluster) ignore the scope.

## The account menu

The card at the bottom of the rail shows your name, or your e-mail when you have not set one, and your role. Open it for a menu about you and nothing else:

- who you are signed in as, and the role you hold **and where it applies** — _Admin · org acme_, _Member · team platform_, _Viewer · project default_, or _Superadmin · whole deployment_. A person with several grants sees the strongest one that reaches the scope the switcher is on;
- **Account & keys**, the screen with your profile, your password, two-factor authentication and your own virtual keys;
- **Preferences**, for your language, default scope, Playground model and time zone;
- **Sign out**.

The menu is a real menu: the arrow keys move through it, **Home** and **End** jump to its ends, **Escape** closes it and returns to the card, and **Tab** leaves it. The scope is not here; it has its own control above (see [the scope switcher](#the-scope-switcher)).

## What a disabled control means

A greyed-out **Add**, **Delete** or toggle carries a tooltip naming the role the action needs (for example _Requires the Admin role_). The dashboard asks the control plane what your account may do and disables the rest up front, instead of letting you click and read a "forbidden" error afterwards. See [Roles and permissions](/security/rbac).

## Refresh and live data

The refresh button in every screen header re-reads everything on the screen. Screens that stream (LLM Logs, MCP Logs, Adaptive Routing) update on their own and show when they last did.

MCP Logs is open to every role and narrowed to your scope: you see the calls of the organizations, teams and projects you hold a role in, plus every call made through your own MCP sign-ins. A call's arguments and result are shown to members and admins, and to viewers on a project that allows it; for anyone else the call opens with a note that they are hidden for your role. **My calls** narrows the list to the calls made under your own account. The figures, the table and the pager all cover one time window, the last 24 hours unless you pick another (last 7 or 30 days, the month so far or the last month) in the window picker; the pick is kept in the address as `?window=7d`, so a link opens on it; if the summary cannot be read, an alert says so with a retry rather than leaving the figures blank. An empty screen links to the MCP Catalog, and a call's request id and trace id open with a copy button, the request id also linking to that request in LLM Logs.

The Dashboard's **Recent requests** card is labelled **Live** once its first read has answered, and asks for new requests every 15 seconds. The figures and the table above it are aggregates over a window, so they are read once a minute. Both pause while the browser tab is hidden and read again when you come back.

Every card on the Dashboard reads for itself: the figures, the spend chart, requests by model and the recent requests. While a card's first read is out it shows placeholders in its own shape, and it never says there is no traffic or nothing logged about a read that has not answered. If that read fails, only that card shows the error, with its own **Try again**, and the cards that loaded stay as they are. A card whose first read failed stops refreshing until you press its **Try again** or the header's refresh button.

When every read fails at once, because the control plane is down or ClickHouse refuses all of them, the Dashboard shows one error in place of the cards instead of four, naming the analytics and quoting the control plane's message. Its **Try again** reads all of them again. The setup checklist above it stays.

If a refresh fails after a card has shown data, the card keeps what it has and says so underneath it, in red: _Refresh failed at 15:34:56, retrying_, until a refresh succeeds. The Recent requests card says it in its label instead, where **Live** gives way to the same sentence. **Live** is only shown while the last read of the card succeeded: while the first read is still out, the label reads **Loading**, and if it fails, the label reads _Load failed at 15:34:56_.

A screen that reads analytics tells a slow query from an unavailable store. When ClickHouse does not answer within the control plane's 15-second limit, the error says the query took too long and suggests a narrower time range, with **Try again**. Any other ClickHouse failure keeps the generic "failed to return" message.

LLM Logs says **Streaming** only after a read that succeeded. While the first page loads, its toolbar says it is loading, and when a read fails it gives the time of the failure instead. If the very first read fails, the screen stops refreshing and keeps the error on screen until you press **Try again**. If a later refresh fails while rows are already showing, the rows stay, and the toolbar reads _Refresh failed at 15:34:56, retrying_ until a refresh succeeds or you pause the stream.

## The gateway status in the header

Next to the refresh button, every screen header shows whether the gateways are taking traffic. Every 30 seconds the dashboard reads the [fleet summary](/api/cluster-summary), which counts the live, stale and config-lagging gateways and is readable by every role, and the pill shows what it counted:

| Pill                       | Meaning                                                                                                                      |
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| _N_ gateways live          | Every live gateway runs the latest config and none has gone quiet. The green dot pulses.                                     |
| _N_ of _M_ gateways behind | Some live gateways have not applied the latest config yet. The dot is amber.                                                 |
| _M_ live · _N_ stale       | Every live gateway is current, but _N_ stopped polling within the last 15 minutes: crashed or partitioned. The dot is amber. |
| no gateway live            | Gateways are in the inventory, and none has polled the control plane in the last minute. The dot is red.                     |

The **Gateways** tile on the Dashboard reads the same summary, so the two never disagree.

When the summary has nothing to count, the pill falls back to the gateway's own `/readyz`, asked through the control plane. That happens when the control plane has no database (`inventory: false`), when no gateway has registered in the inventory (a gateway sends a node id from `ROLTER_NODE_ID` or `HOSTNAME`), and when the request fails, as on a control plane older than the endpoint. The pill then shows what the one gateway that answered said:

| Pill                   | Meaning                                                                                                                   |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| gateway healthy        | The control plane reached the gateway, and it is taking traffic. The green dot pulses.                                    |
| gateway draining       | The gateway was drained from the Cluster screen. It finishes the requests in flight and takes no new ones.                |
| gateway unreachable    | The control plane cannot reach the gateway at `ROLTER_GATEWAY_URL`.                                                       |
| gateway status unknown | The check got no answer the dashboard can read, for example because the browser lost its connection to the control plane. |
| checking gateway       | The first check has not come back yet.                                                                                    |

Hover the pill to see what was counted and when it was last checked. One failed check does not change it: the pill keeps the last answer, stops pulsing, and says when that answer arrived. A second failed check turns it to unknown. Checks pause while the browser tab is hidden, and the refresh button checks again along with everything else.

Every role sees the same pill. The `/readyz` fallback reports only the gateway the control plane forwards to: with several replicas behind one address it means one of them answered, and it cannot tell whether a gateway has picked up the latest configuration. The Cluster screen, for superadmins, lists every node, whether it is live, and whether it runs the current configuration.

## Models, strategies and targets

**Models → Model Catalog** lists every model the gateway serves, the ones shipped in `rolter.toml` and the ones created in the dashboard alike. Each row shows its route's strategy as the identifier you would write in the config file, and how many targets the route spreads traffic across. A strategy with a caveat says so under its name: `precise_cache_aware` and `lmcache_aware` need a cache-telemetry source on their providers, which only a provider defined in `rolter.toml` can carry today (see [Cache-aware routing](/concepts/cache-aware-routing#precise-vllm-and-lmcache-modes)), and `adaptive` is governed by the deployment-wide adaptive-routing policy.

Open the target count to see each target: its provider, the model id sent upstream, and its weight. Only `weighted`, `pipeline` and `adaptive` read weights, so only those routes show each weight as a share of traffic. Under any other strategy the list says the weights do not split traffic. Where the health rollups are available (they need ClickHouse), each target also shows its uptime over the last 7 days against a 99% SLA, the row names how many targets are below it, and a target nothing has observed yet says **no health data**.

The Provider Health screen re-reads its rollups every 30 seconds (the interval pauses while the tab is hidden; the header refresh button reads them at once) and prints when it last did. Each provider card and target row shows when its last event arrived; after 15 minutes without one the line turns to a warning and reads **Stale**. A provider with no events shows no uptime figure rather than 100%.

**Models → Routing Rules** shows each route as a card and follows the same rule: a share and a bar per target only under `weighted`, `pipeline` and `adaptive`, and a note that the weights do not split traffic under any other strategy. Under each target the card states its uptime over the last 7 days in words beside a dot: within SLA, "SLA at risk", "below SLA", or "no health data" for a target nothing has observed. It is the same rollup Model Catalog and the Health screen read, not the gateway's live breaker state. Without the analytics store, or without analytics access, the screen says so once above the cards and the targets show no health.

Under the targets, a card states the requests for the route's model name over the last 24 hours and the share of them that failed, or "No requests" for a model name nothing called. The figure is read for the route's own project, so two projects that define a route with the same model name each show their own number. Both come from the analytics store: a control plane without ClickHouse says so once above the cards, and the cards show no figure rather than a zero. Each enabled card has a **Try it** link that opens the Playground with that route's model selected, and a copy button beside the public model name. A note above the cards counts the live gateways that have not yet picked up the latest configuration, from the same inventory as **Cluster**, and links there.

**+ Add route** takes the name clients will send, the strategy, and one or more targets. The strategy picker groups all 14 strategies (simple, cache-aware, latency and cost, weighted) and gives each a one-line description drawn from [Load Balancing](/concepts/load-balancing); the description of the chosen strategy stays under the picker. `adaptive` is offered like the others: the route opts in, and the deployment-wide policy and kill switch (**Adaptive Routing → Settings**) set how it behaves. The provider group sheet uses the same picker. The button is on Routing Rules and on Model Catalog, and both open the same sheet, so a route is created the same way from either screen.

Each Routing Rules card has an **Edit** button, for roles that may update routes (admin). It opens a sheet with the route's enabled switch, its strategy and its whole target list: add or remove a target, and change a target's provider, upstream model and weight. **Save route** sends the three together in one [route update](/api/routes), which the control plane applies in one transaction, so a refused weight leaves the route as it was. The control plane's refusal of a weight is shown on that target's row. A target you leave alone keeps its identity; a target whose provider or upstream model you change is replaced by a new one. A disabled route is switched back on here. Renaming a route, its default parameters and its pricing stay in the Model Catalog sheet. That sheet saves a route's enabled switch, strategy and targets the same way, in one request, so changing a target's weight there keeps the target too.

Each target is a provider, the model id sent to it, and a weight. A new target starts on no provider when the organization has several, so the route's traffic goes where you chose, not to whichever provider sorts first; with exactly one provider, the target starts on it. The **Upstream model** field suggests the models the picked provider's upstream lists and still takes any id you type. The suggestions come from the provider's own credential, so they need the permission **Test connection** needs; without it the field says suggestions are not available and takes the id as typed. Leave it blank to send the route's own name through.

The sheet opens with no errors on it. A field's error appears once you leave the field. **Add route** with something wrong does not send anything: it shows each problem once, at its field, opens the section that holds it and moves focus to the first, with one line above the buttons saying how many are left.

## When part of a screen fails to load

Some screens read one record per row — the Complexity Router asks for each route's policy separately. If a row's read is delayed or refused, that row is not reported as empty. It is listed under **Policy could not be read**, with the reason from the control plane and a **Try again** button, and it offers no editor: writing a policy over one you were never shown is worse than waiting. Rows whose read is still in flight sit under **Checking for a policy** as placeholders, and the summary line counts only the rows that actually answered.

Routing Rules reads each route's targets separately as well. A card whose targets are still on the way shows placeholders where they will be. A card whose read failed shows the reason and a **Try again** button in their place. Neither says **No targets yet** or counts zero targets. The route count at the top of the screen appears once the route list has answered, so a slow or refused load is never reported as an empty project.

Reading a route's complexity policy takes the same permission as reading the route itself, so a read-only account sees every policy it can see routes for; changing one still takes an admin. An account that may not read the route at all is told it does not have access rather than being shown the route as unconfigured.

## Reading structured output

Anywhere the dashboard shows a payload — a request or response body in LLM Logs and MCP Logs, an audit entry's detail, the effective config, a model's config preview, the collector document, a copy-as-code snippet — it renders in a shared code block: syntax highlighted for its language, scrollable sideways rather than wrapped mid-token, reachable with the Tab key, and with a copy button in its corner. Highlighting is bundled with the dashboard, so it works with no internet access.

A very large payload is shown without colour rather than kept waiting for it.

## The Playground's key

The Playground authenticates to the gateway like any other client, with a virtual key — but you do not paste one. Opening the screen mints a **session key** for the project you have selected:

- It is scoped by the control plane to the models that project routes, so it can never reach a model you could not already reach. A project with no routes yet gets a key for the built-in `fake-llm` alone, so the first message gets an answer before any provider or route exists. The bar says so and links to **Routing Rules**; once the project routes a model, **Renew key** mints a key that reaches it.
- It lives 30 minutes. The bar shows when it expires, and **Renew key** mints a fresh one rather than extending it. When there is no key to renew, the same button reads **Mint key**.
- It is held in your browser's memory only. Closing the tab ends it; nothing is written to browser storage.
- It appears on **Virtual Keys** and in **Settings → Account & keys** labelled _Playground_, so a key with a half-hour life is recognisable as one the screen issued. It expires on its own — there is nothing to clean up.

A new key takes a few seconds to reach the gateway, which learns about keys on its next config poll (every 5 seconds by default, `ROLTER_SNAPSHOT_POLL_SECS`). Until then the model picker says it is waiting and keeps the built-in `fake-llm` selected, asking again with a growing pause for two poll intervals (ten seconds at the default). The wait is sized from `ROLTER_SNAPSHOT_POLL_SECS` as the control plane sees it, so set the same value there as on the gateways. Behind a load balancer the first message can still reach a gateway that has not polled since the key was minted; inside that window a refused message is sent once more before any error is shown. Once the gateway accepts the key, the picker lists what the gateway serves and selects the first route in it.

If the gateway never accepts the key, the picker falls back to the configured routes and says so. That list leaves out routes the control plane is not sending to the gateway (the **Providers** screen lists them as config entries not being served as written), says how many it left out, and opens on a route that is served. When rolter cannot tell which routes those are, it keeps `fake-llm` selected rather than guess.

If the gateway refuses the key, the badge reads **Rejected** rather than **Active**, and the bar suggests minting a fresh one or pasting one the gateway accepts.

The bar says one thing at a time. When the screen cannot mint a key itself, it says why and opens **Use a specific key** for you:

- **No project selected.** Pick one in the scope switcher, or paste a key.
- **Your role cannot mint.** Minting takes the **Member** role in the project. A **Viewer** is not sent into a refusal: the Playground does not try, **Mint key** says which role it takes, and the bar asks for a key someone gave you.

Sign in first — minting is a per-account action, and a deployment running without accounts has none to mint against.

**Send**, and the run buttons on the other tabs, stay disabled until there is a key the gateway accepts: while a new key is on its way, when there is none, and when the gateway refused the one in hand. Hover the button to see why. A gateway that is running on its own with no keys configured, as the no-database `rolter easy-up` does, answers requests without one; the Playground checks for that once and says so, and then sends without a key.

To test one particular key on purpose — a customer's, or one you are about to rotate — open **Use a specific key** and paste it. A pasted key is used exactly as given, with no expiry of rolter's choosing, until you replace it or mint a session key.

## The chat composer

**Enter** sends the message and **Shift+Enter** adds a new line, so a prompt can run over several lines. The box grows with the draft up to about six lines, then scrolls. The **Enter** that confirms a candidate in an input method editor, as when typing Japanese, Chinese or Korean, belongs to the composition and does not send.

A reply is streamed: the column asks the gateway for a server-sent event stream (`stream: true`, with `stream_options.include_usage`) and draws the text as it arrives, so a slow model, such as a reasoning model whose first token takes seconds, shows its answer growing instead of nothing until the end. While the reply is on its way, **Send** is replaced by **Stop**. **Stop**, or **Escape** in the message box, cancels the request and keeps what had arrived, marked _stopped_; the next message then follows that partial reply. Stopping before the first piece arrives puts your message back in the box instead. If the stream breaks part-way, the text received stays, marked _interrupted_, and the error is shown beneath the thread; a failure before any text arrived is a failed send. When the stream ends, the reply is read out to a screen reader once, whole, and the token count from the final chunk and the time it took appear under it. A gateway or upstream that sends no usage chunk shows the time alone. The call counts as in flight until the stream ends, so a session key is not renewed under a reply that is still arriving.

Each send carries the whole conversation in its column, your earlier messages and the model's replies included, so a follow-up question is answered in context. Switching the column to another model keeps the conversation, and the next message goes to the new model with all of it. While **Multimodal** is on, an image goes out again with the message it was attached to on every later send, and that message is marked _image attached_. Switch it off and later sends carry text only. If a send fails, the conversation stays as it was before it and your message goes back into the box, so **Enter** sends it again; anything you typed while waiting stays, after it. The eraser button in the column header, **Clear the conversation**, starts the column over and keeps a draft you have not sent. **Copy as code** writes out the last request the column sent, with every message in it and without the reply that came back; an image attached to a message stays in the snippet as an image part, but its data is not pasted in: the snippet reads it from a file (`image.png`, or `image-1.png`, `image-2.png` when several turns carry one), so save the picture under that name next to where you run it.

Each chat column keeps its conversation, and any draft you have not sent, while you visit another tab. So do the other tabs: Embeddings keeps the texts you edited and their projection, Image keeps the prompt and the generated images, and Audio keeps the text to synthesize and the clip, or the uploaded file's transcript. A clip that is playing pauses when you leave. A request still on its way when you leave lands where it started, so an image you are waiting on is there when you come back, and coming back never sends it again. Nothing is saved: all of it lives in the page, so reloading or leaving the Playground clears it.

Realtime is the exception. Leaving its tab closes the session and clears the event log, because a session kept alive under a tab you are not looking at would hold a WebSocket open. The tab says so above the session.

A screen reader hears each finished reply once it has arrived, as the model's name followed by the reply, and hears a failed send's error as an alert. Every field has a name of its own, not only a placeholder. Each column's conversation is a region named after its model: press Tab to reach it, then the arrow keys or Page Down to scroll a long reply. The buttons a column repeats say what they act on, as in **Send to** a model and **Copy as code for** a model. In a comparison they also carry the column's number, and **Remove column** carries its number and model, so two columns holding the same model stay apart. On Embeddings each row's **Remove text** button carries the row's number.

## Markdown in the Playground

Model replies render as markdown: headings, lists, tables, inline code, and fenced code blocks highlighted per their language tag with a copy button of their own. Use the **¶** button in the column header to switch to raw output when the question is exactly what the model emitted, including stray delimiters and whitespace.

Replies are never treated as markup. HTML tags in a reply show as the characters they are, and an image in a reply is listed by its alt text instead of being fetched — nothing in a model reply causes the dashboard to reach the network.

## On a phone or a tablet

| Screen width   | Navigation                                                                                                                                  |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| Below 768 px   | The rail is a drawer. Open it with the **☰** button in the screen header; close it with the scrim, the Escape key, or by picking a screen. |
| 768 – 1023 px  | The rail starts folded to icons. Hover or focus an icon for its name; a group's icon opens its screens; the collapse toggle expands it.     |
| 1024 px and up | The full rail, collapsible, and resizable by dragging its right edge. The width you choose is remembered.                                   |

Tables that are wider than the screen scroll inside their own frame, and forms that open as side sheets take the full width on a phone. On a phone a side sheet's buttons stack: Cancel and the main action share the bottom line, and any other action, such as testing a provider's connection, sits above them. The screen area below the header is a scroll region you can Tab to, so a long screen can be read with the keyboard alone.

A dialog taller than the window, on a short laptop screen or at 200 % zoom, opens with its title and close button on screen and scrolls. The longer form dialogs, such as editing a guardrail rule, a plugin or an MCP server, keep their buttons on screen too and scroll only the fields in between.

## With a screen reader

The list screens (Virtual Keys, Users, Model Providers, Provider Groups, the Model Catalog, Alert History, MCP Logs, OAuth Grants and Auth Sessions) are tables to a screen reader, named after the screen, so table navigation works and each cell is read with its column header. A column you can sort says whether it is sorted ascending, descending or not at all. The column of row buttons has no visible heading and is announced as **Row actions**; on Auth Sessions the two button columns are **Renew** and **Revoke**. On MCP Logs that column is **Details**: each row's button is named after the tool, the server and the time of the call, and it opens the call's arguments and result. Focus moves into the details panel, and Escape or the panel's close button returns it to that button.
