<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
<title>LiteLLM-RS Admin</title>
<link rel="stylesheet" href="/admin/dashboard/app.css">
<script src="/admin/dashboard/provider-health.js" defer></script>
<script src="/admin/dashboard/routing-inventory.js" defer></script>
<script src="/admin/dashboard/request-ledger.js" defer></script>
<script src="/admin/dashboard/budget.js" defer></script>
<script src="/admin/dashboard/providers.js" defer></script>
<script src="/admin/dashboard/routing-policy.js" defer></script>
<script src="/admin/dashboard/app.js" defer></script>
</head>
<body>
<a class="skip-link" href="#main-content">Skip to content</a>
<header class="topbar">
<div>
<p class="eyebrow">LiteLLM-RS</p>
<h1>Admin dashboard</h1>
</div>
<div class="session-actions">
<span id="session-label">Signed out</span>
<button id="sign-out" class="secondary" type="button" hidden>Sign out</button>
</div>
</header>
<main id="main-content">
<section id="login-panel" class="auth-card" aria-labelledby="login-title">
<div>
<p class="eyebrow">Secure access</p>
<h2 id="login-title">Sign in as an administrator</h2>
<p>Credentials are sent to the existing gateway login endpoint. Tokens
stay in this tab's memory and are cleared on reload.</p>
</div>
<form id="login-form">
<label for="username">Username</label>
<input id="username" name="username" autocomplete="username" required>
<label for="password">Password</label>
<input id="password" name="password" type="password"
autocomplete="current-password" required>
<button type="submit">Sign in</button>
</form>
</section>
<section id="dashboard-shell" hidden>
<nav class="tabs" aria-label="Dashboard sections">
<button class="tab active" type="button" data-view="keys"
aria-controls="keys-panel" aria-selected="true">API keys</button>
<button class="tab" type="button" data-view="teams"
aria-controls="teams-panel" aria-selected="false">Teams</button>
<button class="tab" type="button" data-view="spend"
aria-controls="spend-panel" aria-selected="false">Spend</button>
<button class="tab" type="button" data-view="budgets"
aria-controls="budgets-panel" aria-selected="false">Budgets</button>
<button class="tab" type="button" data-view="providers"
aria-controls="providers-panel" aria-selected="false">Providers</button>
<button class="tab" type="button" data-view="health"
aria-controls="health-panel" aria-selected="false">Provider health</button>
<button class="tab" type="button" data-view="routing"
aria-controls="routing-panel" aria-selected="false">Routing inventory</button>
<button class="tab" type="button" data-view="routing-policy"
aria-controls="routing-policy-panel" aria-selected="false">Routing policy</button>
<button class="tab" type="button" data-view="request-logs"
aria-controls="request-logs-panel" aria-selected="false">Request logs</button>
</nav>
<section id="keys-panel" class="panel" aria-labelledby="keys-title">
<div class="panel-heading">
<div>
<p class="eyebrow">Credentials</p>
<h2 id="keys-title">API keys</h2>
</div>
<button id="refresh-keys" class="secondary" type="button">Refresh</button>
</div>
<details class="create-card">
<summary>Create a scoped key</summary>
<form id="create-key-form" class="form-grid">
<label for="key-name">Name</label>
<input id="key-name" name="name" required maxlength="120">
<label for="key-description">Description</label>
<input id="key-description" name="description" maxlength="500">
<label for="key-team">Owner</label>
<select id="key-team" name="team_id">
<option value="">My administrator user</option>
</select>
<label for="key-models">Allowed model patterns</label>
<input id="key-models" name="allowed_models" required
placeholder="gpt-4o, claude-*"
aria-describedby="key-scope-help">
<label for="key-endpoints">Allowed endpoint patterns</label>
<input id="key-endpoints" name="allowed_endpoints" required
placeholder="/v1/chat/completions, /v1/embeddings"
aria-describedby="key-scope-help">
<p id="key-scope-help" class="field-help">Comma-separated values are
required. The unrestricted <code>*</code> value is not accepted.</p>
<button type="submit">Create key</button>
</form>
</details>
<div class="table-wrap">
<table>
<caption>Keys visible to the signed-in administrator</caption>
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Prefix</th>
<th scope="col">Owner</th>
<th scope="col">Status</th>
<th scope="col">Created</th>
<th scope="col">Action</th>
</tr>
</thead>
<tbody id="keys-body"></tbody>
</table>
</div>
<p id="keys-empty" class="empty-state" hidden>No API keys found.</p>
<div class="pagination" aria-label="API key pages">
<button id="keys-previous" class="secondary" type="button">Previous</button>
<span id="keys-page">Page 1</span>
<button id="keys-next" class="secondary" type="button">Next</button>
</div>
</section>
<section id="teams-panel" class="panel" aria-labelledby="teams-title" hidden>
<div class="panel-heading">
<div>
<p class="eyebrow">Ownership</p>
<h2 id="teams-title">Teams</h2>
</div>
<button id="refresh-teams" class="secondary" type="button">Refresh</button>
</div>
<details class="create-card">
<summary>Create a team</summary>
<form id="create-team-form" class="form-grid">
<label for="team-name">Unique name</label>
<input id="team-name" name="name" required
pattern="[A-Za-z0-9_-]+" maxlength="120"
aria-describedby="team-name-help"
title="Use only letters, numbers, underscores, and hyphens.">
<p id="team-name-help" class="field-help">Use only letters, numbers,
underscores, and hyphens.</p>
<label for="team-display-name">Display name</label>
<input id="team-display-name" name="display_name" maxlength="120">
<label for="team-description">Description</label>
<input id="team-description" name="description" maxlength="500">
<button type="submit">Create team</button>
</form>
</details>
<div class="table-wrap">
<table>
<caption>Teams visible to the signed-in administrator</caption>
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Display name</th>
<th scope="col">Status</th>
<th scope="col">Members</th>
<th scope="col">Action</th>
</tr>
</thead>
<tbody id="teams-body"></tbody>
</table>
</div>
<p id="teams-empty" class="empty-state" hidden>No teams found.</p>
<div class="pagination" aria-label="Team pages">
<button id="teams-previous" class="secondary" type="button">Previous</button>
<span id="teams-page">Page 1</span>
<button id="teams-next" class="secondary" type="button">Next</button>
</div>
</section>
<section id="spend-panel" class="panel" aria-labelledby="spend-title" hidden>
<div class="panel-heading">
<div>
<p class="eyebrow">Current pages</p>
<h2 id="spend-title">Usage and spend</h2>
<p>Key and team scopes are shown separately to avoid double-counting.</p>
</div>
<button id="refresh-spend" class="secondary" type="button">Refresh</button>
</div>
<div class="spend-grid">
<div class="table-wrap">
<table>
<caption>Spend for API keys on the current key page</caption>
<thead>
<tr>
<th scope="col">Key</th>
<th scope="col">Today</th>
<th scope="col">Total</th>
<th scope="col">Requests</th>
<th scope="col">Tokens</th>
</tr>
</thead>
<tbody id="key-spend-body"></tbody>
</table>
</div>
<p id="key-spend-empty" class="empty-state" hidden>
No API keys on the current page.
</p>
<div class="table-wrap">
<table>
<caption>Spend for teams on the current team page</caption>
<thead>
<tr>
<th scope="col">Team</th>
<th scope="col">Today</th>
<th scope="col">Total</th>
<th scope="col">Requests</th>
<th scope="col">Tokens</th>
</tr>
</thead>
<tbody id="team-spend-body"></tbody>
</table>
</div>
<p id="team-spend-empty" class="empty-state" hidden>
No teams on the current page.
</p>
</div>
</section>
<section id="budgets-panel" class="panel" aria-labelledby="budgets-title" hidden>
<div class="panel-heading">
<div>
<p class="eyebrow">Cost controls</p>
<h2 id="budgets-title">Provider and model budgets</h2>
<p>Set enforced gateway limits and monitor remaining spend.</p>
</div>
<button id="refresh-budgets" class="secondary" type="button">Refresh</button>
</div>
<details id="budget-editor" class="create-card">
<summary>Create or update a budget</summary>
<form id="budget-form" class="form-grid">
<label for="budget-scope">Scope</label>
<select id="budget-scope" name="scope">
<option value="provider">Provider</option>
<option value="model">Model</option>
</select>
<label id="budget-name-label" for="budget-name">Provider name</label>
<input id="budget-name" name="name" required maxlength="240"
placeholder="openai">
<label for="budget-max">Maximum budget</label>
<input id="budget-max" name="max_budget" type="number" required
step="any" inputmode="decimal">
<label for="budget-reset-period">Reset period</label>
<select id="budget-reset-period" name="reset_period">
<option value="daily">Daily</option>
<option value="weekly">Weekly</option>
<option value="monthly" selected>Monthly</option>
<option value="never">Never</option>
</select>
<label for="budget-currency">Currency</label>
<select id="budget-currency" name="currency">
<option value="USD">USD</option>
</select>
<label class="checkbox-row" for="budget-enabled">
<input id="budget-enabled" name="enabled" type="checkbox" checked>
Enforce this budget
</label>
<p class="field-help">Saving an existing scope updates its settings
without resetting accrued spend or its warning threshold.</p>
<button type="submit">Save budget</button>
<button id="cancel-budget-edit" class="secondary" type="button" hidden>
Cancel edit
</button>
</form>
</details>
<div class="budget-grid">
<div>
<h3>Providers</h3>
<div class="table-wrap">
<table>
<caption>Provider budget limits</caption>
<thead>
<tr>
<th scope="col">Provider</th>
<th scope="col">Current</th>
<th scope="col">Remaining</th>
<th scope="col">Status</th>
<th scope="col">Reset</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody id="provider-budgets-body"></tbody>
</table>
</div>
<p id="provider-budgets-empty" class="empty-state" hidden>
No provider budgets configured.
</p>
</div>
<div>
<h3>Models</h3>
<div class="table-wrap">
<table>
<caption>Model budget limits</caption>
<thead>
<tr>
<th scope="col">Model</th>
<th scope="col">Current</th>
<th scope="col">Remaining</th>
<th scope="col">Status</th>
<th scope="col">Reset</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody id="model-budgets-body"></tbody>
</table>
</div>
<p id="model-budgets-empty" class="empty-state" hidden>
No model budgets configured.
</p>
</div>
</div>
</section>
<section id="providers-panel" class="panel" aria-labelledby="providers-title" hidden>
<div class="panel-heading">
<div>
<p class="eyebrow">Runtime configuration</p>
<h2 id="providers-title">Providers</h2>
<p>Create, edit, disable, or delete providers. Secrets accept
replacement references only and are never loaded from GET.</p>
</div>
<button id="refresh-providers" class="secondary" type="button">Refresh</button>
</div>
<details class="create-card">
<summary>Create a provider</summary>
<form id="create-provider-form" class="form-grid">
<label for="provider-create-name">Name</label>
<input id="provider-create-name" name="name" required maxlength="120">
<label for="provider-create-type">Type</label>
<input id="provider-create-type" name="provider_type" required
maxlength="120" placeholder="openai">
<label for="provider-create-api-key">API key</label>
<input id="provider-create-api-key" name="api_key"
placeholder="replacement reference only"
autocomplete="off">
<p class="field-help">Use a ${ENV_VAR} reference. Raw keys are rejected.</p>
<label for="provider-create-base-url">Base URL</label>
<input id="provider-create-base-url" name="base_url" maxlength="500">
<label for="provider-create-models">Models</label>
<input id="provider-create-models" name="models"
placeholder="gpt-4o, gpt-4o-mini">
<label for="provider-create-tags">Tags</label>
<input id="provider-create-tags" name="tags">
<label for="provider-create-weight">Weight</label>
<input id="provider-create-weight" name="weight" type="number"
step="any" value="1">
<label for="provider-create-priority">Priority</label>
<input id="provider-create-priority" name="priority" type="number"
step="1" value="0">
<label class="checkbox-row" for="provider-create-enabled">
<input id="provider-create-enabled" name="enabled" type="checkbox" checked>
Enabled
</label>
<button type="submit">Create provider</button>
</form>
</details>
<details id="provider-editor" class="create-card" hidden>
<summary>Edit provider</summary>
<form id="edit-provider-form" class="form-grid">
<label for="provider-edit-name">Name</label>
<input id="provider-edit-name" name="name" required maxlength="120"
readonly>
<label for="provider-edit-type">Type</label>
<input id="provider-edit-type" name="provider_type" required
maxlength="120">
<label for="provider-edit-api-key">API key</label>
<input id="provider-edit-api-key" name="api_key"
placeholder="replacement reference only"
autocomplete="off">
<p id="provider-edit-api-key-ref" class="field-help"></p>
<label for="provider-edit-base-url">Base URL</label>
<input id="provider-edit-base-url" name="base_url" maxlength="500">
<label for="provider-edit-models">Models</label>
<input id="provider-edit-models" name="models">
<label for="provider-edit-tags">Tags</label>
<input id="provider-edit-tags" name="tags">
<label for="provider-edit-weight">Weight</label>
<input id="provider-edit-weight" name="weight" type="number" step="any">
<label for="provider-edit-priority">Priority</label>
<input id="provider-edit-priority" name="priority" type="number" step="1">
<label class="checkbox-row" for="provider-edit-enabled">
<input id="provider-edit-enabled" name="enabled" type="checkbox">
Enabled
</label>
<button type="submit">Save provider</button>
<button id="cancel-provider-edit" class="secondary" type="button">
Cancel edit
</button>
</form>
</details>
<p id="providers-notice" class="health-notice" aria-live="polite"></p>
<div class="table-wrap">
<table>
<caption>Configured providers from the live runtime</caption>
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Type</th>
<th scope="col">Enabled</th>
<th scope="col">Models</th>
<th scope="col">Secret</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody id="providers-body"></tbody>
</table>
</div>
<p id="providers-empty" class="empty-state" hidden>No providers configured.</p>
</section>
<section id="health-panel" class="panel" aria-labelledby="health-title" hidden>
<div class="panel-heading">
<div>
<p class="eyebrow">Live gateway snapshot</p>
<h2 id="health-title">Provider health</h2>
<p>Enabled providers and the latest upstream probe evidence.</p>
</div>
<button id="refresh-health" class="secondary" type="button">Refresh</button>
</div>
<p id="health-summary" class="health-summary"></p>
<p id="health-notice" class="health-notice" aria-live="polite"></p>
<div class="table-wrap">
<table>
<caption>Configured provider health from the gateway</caption>
<thead>
<tr>
<th scope="col">Provider</th>
<th scope="col">Availability</th>
<th scope="col">Probe</th>
<th scope="col">Last probe</th>
<th scope="col">Detail</th>
</tr>
</thead>
<tbody id="health-body"></tbody>
</table>
</div>
<p id="health-empty" class="empty-state" hidden>No providers configured.</p>
</section>
<section id="routing-panel" class="panel" aria-labelledby="routing-title" hidden>
<div class="panel-heading">
<div>
<p class="eyebrow">Live routing snapshot</p>
<h2 id="routing-title">Routing inventory</h2>
<p>Public models, deployments, and configured providers that are not routed.</p>
</div>
<button id="refresh-routing" class="secondary" type="button">Refresh</button>
</div>
<p id="routing-summary" class="health-summary"></p>
<p id="routing-notice" class="health-notice" aria-live="polite"></p>
<div class="table-wrap">
<table>
<caption>Deployments from the live routing snapshot</caption>
<thead>
<tr>
<th scope="col">Public model</th>
<th scope="col">Aliases</th>
<th scope="col">Provider</th>
<th scope="col">Deployment</th>
<th scope="col">Wire model</th>
<th scope="col">Capabilities</th>
<th scope="col">Health</th>
<th scope="col">Availability</th>
<th scope="col">Unavailable reasons</th>
<th scope="col">Cooldown</th>
<th scope="col">RPM</th>
<th scope="col">TPM</th>
<th scope="col">Active</th>
</tr>
</thead>
<tbody id="routing-body"></tbody>
</table>
</div>
<p id="routing-empty" class="empty-state" hidden>No routed deployments.</p>
<div class="table-wrap">
<table>
<caption>Configured providers missing from the live snapshot</caption>
<thead>
<tr>
<th scope="col">Provider</th>
<th scope="col">Type</th>
<th scope="col">Public models</th>
<th scope="col">Availability</th>
<th scope="col">Unavailable reasons</th>
</tr>
</thead>
<tbody id="routing-unavailable-body"></tbody>
</table>
</div>
<p id="routing-unavailable-empty" class="empty-state" hidden>
No configured providers are missing from routing.
</p>
</section>
<section id="routing-policy-panel" class="panel" aria-labelledby="routing-policy-title" hidden>
<div class="panel-heading">
<div>
<p class="eyebrow">Runtime configuration</p>
<h2 id="routing-policy-title">Routing policy</h2>
<p>Edit strategy, circuit breaker, load balancer, aliases, and
named provider weight/priority. Unknown fields are not sent.</p>
</div>
<button id="refresh-routing-policy" class="secondary" type="button">Refresh</button>
</div>
<p id="routing-policy-generation" class="health-summary">Active revision: —</p>
<p id="routing-policy-notice" class="health-notice" aria-live="polite"></p>
<form id="routing-policy-form">
<div class="form-grid">
<label for="routing-policy-strategy">Strategy</label>
<select id="routing-policy-strategy" name="strategy">
<option value="simple_shuffle">simple_shuffle</option>
<option value="least_busy">least_busy</option>
<option value="usage_based">usage_based</option>
<option value="latency_based">latency_based</option>
<option value="priority_based">priority_based</option>
<option value="rate_limit_aware">rate_limit_aware</option>
<option value="round_robin">round_robin</option>
</select>
<label for="routing-policy-failure-threshold">Failure threshold</label>
<input id="routing-policy-failure-threshold" name="failure_threshold"
type="number" min="0" step="1">
<label for="routing-policy-recovery-timeout">Recovery timeout</label>
<input id="routing-policy-recovery-timeout" name="recovery_timeout"
type="number" min="0" step="1">
<label for="routing-policy-min-requests">Minimum requests</label>
<input id="routing-policy-min-requests" name="min_requests"
type="number" min="0" step="1">
<label for="routing-policy-success-threshold">Success threshold</label>
<input id="routing-policy-success-threshold" name="success_threshold"
type="number" min="0" step="1">
<label class="checkbox-row" for="routing-policy-health-check-enabled">
<input id="routing-policy-health-check-enabled" name="health_check_enabled"
type="checkbox" checked>
Health checks enabled
</label>
<label class="checkbox-row" for="routing-policy-sticky-sessions">
<input id="routing-policy-sticky-sessions" name="sticky_sessions"
type="checkbox">
Sticky sessions
</label>
<label for="routing-policy-session-timeout">Session timeout</label>
<input id="routing-policy-session-timeout" name="session_timeout"
type="number" min="0" step="1">
</div>
<h3>Model aliases</h3>
<div class="table-wrap">
<table>
<caption>Name to canonical model target</caption>
<thead>
<tr>
<th scope="col">Alias</th>
<th scope="col">Target</th>
<th scope="col">Action</th>
</tr>
</thead>
<tbody id="routing-policy-aliases-body"></tbody>
</table>
</div>
<button id="routing-policy-add-alias" class="secondary" type="button">
Add alias
</button>
<h3>Provider weight and priority</h3>
<div class="table-wrap">
<table>
<caption>Named providers from the live policy</caption>
<thead>
<tr>
<th scope="col">Provider</th>
<th scope="col">Weight</th>
<th scope="col">Priority</th>
</tr>
</thead>
<tbody id="routing-policy-providers-body"></tbody>
</table>
</div>
<p id="routing-policy-providers-empty" class="empty-state" hidden>
No named providers in the current policy.
</p>
<button type="submit">Apply routing policy</button>
</form>
</section>
<section id="request-logs-panel" class="panel" aria-labelledby="request-logs-title" hidden>
<div class="panel-heading">
<div>
<p class="eyebrow">Metadata only</p>
<h2 id="request-logs-title">Request logs</h2>
<p>Terminal request outcomes without prompt or response bodies.</p>
</div>
<button id="refresh-request-logs" class="secondary" type="button">Refresh</button>
</div>
<form id="request-logs-filter" class="form-grid">
<label for="request-logs-after">Finished after</label>
<input id="request-logs-after" name="finished_after" type="datetime-local">
<label for="request-logs-before">Finished before</label>
<input id="request-logs-before" name="finished_before" type="datetime-local">
<label for="request-logs-request-id">Request ID</label>
<input id="request-logs-request-id" name="request_id" maxlength="120">
<label for="request-logs-model">Model</label>
<input id="request-logs-model" name="model" maxlength="240">
<label for="request-logs-provider">Provider</label>
<input id="request-logs-provider" name="provider" maxlength="120">
<label for="request-logs-status">Status</label>
<select id="request-logs-status" name="terminal_status">
<option value="">Any</option>
<option value="completed">Completed</option>
<option value="failed">Failed</option>
<option value="cancelled">Cancelled</option>
</select>
<button type="submit">Apply filters</button>
</form>
<p id="request-logs-summary" class="health-summary"></p>
<p id="request-logs-notice" class="health-notice" aria-live="polite"></p>
<div class="table-wrap">
<table>
<caption>Metadata-only terminal request records</caption>
<thead>
<tr>
<th scope="col">Request ID</th>
<th scope="col">Finished</th>
<th scope="col">Model</th>
<th scope="col">Provider</th>
<th scope="col">Status</th>
<th scope="col">Code</th>
<th scope="col">Latency</th>
<th scope="col">Cost</th>
<th scope="col">API key</th>
<th scope="col">Action</th>
</tr>
</thead>
<tbody id="request-logs-body"></tbody>
</table>
</div>
<p id="request-logs-empty" class="empty-state" hidden>No request logs found.</p>
<div class="pagination" aria-label="Request log pages">
<button id="request-logs-previous" class="secondary" type="button">Previous</button>
<button id="request-logs-next" class="secondary" type="button">Next</button>
</div>
<div id="request-logs-detail" hidden></div>
</section>
</section>
</main>
<div id="status-region" class="status" aria-live="polite"></div>
<div id="error-region" class="error" role="alert" hidden></div>
<dialog id="raw-key-dialog" aria-labelledby="raw-key-title">
<h2 id="raw-key-title">Copy this key now</h2>
<p>The raw key is shown once and is not retained after this dialog closes.</p>
<pre id="raw-key-value" tabindex="0"></pre>
<div class="dialog-actions">
<button id="copy-raw-key" type="button">Copy key</button>
<button id="dismiss-raw-key" class="secondary" type="button">Dismiss</button>
</div>
</dialog>
</body>
</html>