Files
llmgw/internal/web/templates/admin_quotas.html
T
Ilker ManapandClaude Opus 5.5 02fa639657 Open the mirror's registry on what is current, not an empty search box
The mirror page lists Hugging Face's trending models without anything
typed, with views for what is new this month and what is most
downloaded, filters for task and size, and each repository's parameter
count, architecture, context length and age from one registry call.
"Only what these nodes can run" is on by default and leaves out
architectures the nodes' llama.cpp cannot load, and adapters, saying how
many it hid. Listings are remembered for ten minutes.

The stylesheet is now linked by a hash of its content. It is cached for
a day, so under a fixed name a deploy's new styles reached people a day
after its markup.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 21:56:58 +02:00

196 lines
10 KiB
HTML

{{define "admin_quotas"}}<!doctype html>
<html lang="en"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Quotas · llmgw</title><link rel="stylesheet" href="{{stylesheet}}">
{{/* Links and forms work as plain navigation; htmx, when it loads, swaps only
#quotas so opening a form or saving one does not jump the page. */}}
<meta name="htmx-config" content='{"scrollIntoViewOnBoost":false,"defaultFocusScroll":false}'>
<script src="/_static/htmx-2.0.11.min.js" defer></script>
</head><body><div class="wrap">
<header>
<h1>Quotas</h1>
<p class="sub">What each organization, team and project may use · spend over the last {{.Since}}</p>
{{template "nav" .Nav}}
</header>
<div id="quotas" hx-boost="true" hx-target="#quotas" hx-select="#quotas"
hx-swap="outerHTML" hx-push-url="false">
{{if not .BudgetsBind}}
{{/* A budget is spend per period and spend comes from a price table. With no
prices every request costs nothing, so these numbers read as limits and
bind on nothing. Self-hosted models having no per-token price is ordinary;
showing somebody a budget without saying so is not. */}}
<p class="banner-warn" role="note">No model has a price, so nothing is charged and
budgets stay at zero however much is used. Rate and token limits still apply. Set
prices in the <code>pricing</code> block of the server configuration to make
budgets bind.</p>
{{end}}
{{if .Error}}<p class="error" role="alert">{{.Error}}</p>{{end}}
{{if .Message}}<p class="ok" role="status">{{.Message}}</p>{{end}}
{{if .Rows}}
{{/* The tree is the interface. A quota belongs to a branch, so the form to set
one opens on that branch rather than asking somebody to name it again in a
box at the bottom. */}}
<table class="quota-tree">
<tr><th>Where</th><th class="num">Requests/min</th><th class="num">Tokens/min</th>
<th class="num">Budget</th><th>Spent</th><th></th></tr>
{{range .Rows}}
<tr class="q-depth-{{.Indent}}{{if .Editing}} editing{{end}}">
<td class="q-where">
<span class="q-name">{{.Path}}</span> <span class="q-kind">{{.Scope}}</span>
{{if .Models}}<div class="q-sub">only <code>{{.Models}}</code></div>
{{else if .ModelsFrom}}<div class="q-sub">models limited by {{.ModelsFrom}}</div>{{end}}
{{if .Member}}<div class="q-sub">each member here: {{.Member}}</div>{{end}}
{{if .Shadowed}}
{{/* A value that is written here and never reached. Resolution takes the
most restrictive along the chain, so without saying so the page would
show a number as though it applied. */}}
<div class="q-sub q-warn">{{.Shadowed}}</div>{{end}}
</td>
<td class="num">{{template "quota-value" dict "Own" .RPM "Up" .RPMUp "From" .RPMFrom}}</td>
<td class="num">{{template "quota-value" dict "Own" .TPM "Up" .TPMUp "From" .TPMFrom}}</td>
<td class="num">{{if .Budget}}{{.Budget}}{{if .Period}}<span class="q-per">/{{.Period}}</span>{{end}}
{{else}}{{template "quota-value" dict "Own" "" "Up" .BudgetUp "From" .BudgetFrom}}{{end}}</td>
<td class="q-spent">
<span class="q-spend">{{.Spend}}</span>
{{if .HasPct}}<span class="q-meter{{if ge .Percent 90}} hot{{end}}" title="{{.Percent}}% of its budget"><i style="width:{{pct .Percent 100}}%"></i></span>
<span class="q-pct">{{.Percent}}%</span>{{end}}
</td>
<td class="q-act">
{{if .Editing}}<a href="/admin/quotas">close</a>
{{else}}<a href="/admin/quotas?edit={{.Scope}}:{{.ID}}">{{if .HasAny}}change{{else}}set{{end}}</a>{{end}}
{{if .MemberEditing}}<a href="/admin/quotas">close</a>
{{else}}<a href="/admin/quotas?edit={{.Scope}}-member:{{.ID}}" title="A limit each member here has separately">per member</a>{{end}}
</td>
</tr>
{{if .MemberEditing}}
<tr class="editing"><td colspan="6">
{{template "quota-form" (dict "Scope" (printf "%s-member" .Scope) "ID" .ID "F" .MemberRaw "CSRF" $.CSRF
"Title" (printf "Each member of %s, separately" .Crumb)
"Note" "Every member gets this much here, counted apart from the others — a fair share rather than one pool.")}}
</td></tr>
{{end}}
{{if .Editing}}
<tr class="editing"><td colspan="6">
<form method="post" action="/admin/quotas" class="q-form">
<input type="hidden" name="csrf" value="{{$.CSRF}}">
<input type="hidden" name="scope" value="{{.Scope}}">
<input type="hidden" name="id" value="{{.ID}}">
<p class="q-form-title">Quota for <strong>{{.Crumb}}</strong>, shared by everything beneath it.
A blank field means no limit of that kind, not a limit of zero.</p>
<div class="field-row">
<label>Requests / minute
<input type="text" name="rpm" value="{{.RPM}}" placeholder="{{if .RPMUp}}{{.RPMUp}} from {{.RPMFrom}}{{else}}none{{end}}"></label>
<label>Tokens / minute
<input type="text" name="tpm" value="{{.TPM}}" placeholder="{{if .TPMUp}}{{.TPMUp}} from {{.TPMFrom}}{{else}}none{{end}}"></label>
<label>Budget
<input type="text" name="budget" value="{{.Budget}}" placeholder="{{if .BudgetUp}}{{.BudgetUp}} from {{.BudgetFrom}}{{else}}none{{end}}"></label>
<label class="q-period">per
<select name="period">
<option value="month"{{if eq .Period "month"}} selected{{end}}>month</option>
<option value="day"{{if eq .Period "day"}} selected{{end}}>day</option>
</select></label>
</div>
<label>Only these models <span class="opt">comma separated; blank allows all</span>
<input type="text" name="models" value="{{.Models}}" placeholder="every model"></label>
<div class="q-form-actions">
<button type="submit">Save</button>
<a href="/admin/quotas">Cancel</a>
<button type="submit" name="clear" value="1" class="link danger"
{{if not .HasAny}}disabled{{end}}>Remove every limit here</button>
</div>
</form>
</td></tr>
{{end}}
{{end}}
</table>
<p class="q-legend"><span class="q-up">60 ↑</span> is held by a limit set higher
up — hover it to see where. A value set here is in normal type.</p>
{{else}}
<p class="empty">Nothing to give a quota to yet. Make an organization on the
<a href="/admin/users">people page</a> first.</p>
{{end}}
<h2 id="people-limits">People</h2>
<p class="note">A person's own limit covers everything they do, across all their
keys, projects and the chat page. {{.Personal}}</p>
{{if .People}}
<table class="quota-tree people-limits">
<tr><th>Person</th><th class="num">Requests/min</th><th class="num">Tokens/min</th>
<th class="num">Budget</th><th>Spent</th><th></th></tr>
{{range .People}}
<tr{{if .Editing}} class="editing"{{end}}>
<td class="q-where"><span class="q-name">{{.Email}}</span>
{{if .Models}}<div class="q-sub">only <code>{{.Models}}</code></div>{{end}}</td>
<td class="num">{{or .RPM "—"}}</td>
<td class="num">{{or .TPM "—"}}</td>
<td class="num">{{if .Budget}}{{.Budget}}{{if .Period}}<span class="q-per">/{{.Period}}</span>{{end}}{{else}}—{{end}}</td>
<td class="q-spent"><span class="q-spend">{{.Spend}}</span>
{{if .HasPct}}<span class="q-meter{{if ge .Percent 90}} hot{{end}}"><i style="width:{{pct .Percent 100}}%"></i></span>
<span class="q-pct">{{.Percent}}%</span>{{end}}</td>
<td class="q-act">{{if .Editing}}<a href="/admin/quotas">close</a>
{{else}}<a href="/admin/quotas?edit=user:{{.ID}}">{{if .HasAny}}change{{else}}set{{end}}</a>{{end}}</td>
</tr>
{{if .Editing}}
<tr class="editing"><td colspan="6">
{{template "quota-form" (dict "Scope" "user" "ID" .ID "CSRF" $.CSRF
"F" (dict "RPM" .RPM "TPM" .TPM "Budget" .Budget "Period" .Period "Models" .Models "HasAny" .HasAny)
"Title" (printf "%s, everywhere" .Email)
"Note" "Across every key this person holds, every project they use and the chat page.")}}
</td></tr>
{{end}}
{{end}}
</table>
{{else}}<p class="empty">Nobody has been approved yet.</p>{{end}}
<details class="explain">
<summary>How limits combine</summary>
<p>Along a branch the most restrictive value wins, so a team cannot raise what
its organization set and a project cannot raise its team's. Only an
organization's own limit is bounded by nothing. Model lists intersect: a
project cannot allow a model its team withheld.</p>
<p>Per-key limits are not set here: a key is bounded by its project, and
putting every caller's throughput one click from an admin session is more
than this page should do — <code>llmgw keys limits --scope key</code>.</p>
</details>
</div>
<footer><span>llmgw {{.Version}}</span><span>{{.Now}}</span></footer>
</div></body></html>{{end}}
{{/* A limit as the table shows it: the row's own value, else the one it is
held to from above, marked as inherited and saying from where. */}}
{{/* The form for a limit that is not a branch's own: per member, or a person. */}}
{{define "quota-form"}}
<form method="post" action="/admin/quotas" class="q-form">
<input type="hidden" name="csrf" value="{{.CSRF}}">
<input type="hidden" name="scope" value="{{.Scope}}">
<input type="hidden" name="id" value="{{.ID}}">
<p class="q-form-title"><strong>{{.Title}}</strong>. {{.Note}} A blank field means no limit of that kind.</p>
<div class="field-row">
<label>Requests / minute <input type="text" name="rpm" value="{{.F.RPM}}" placeholder="none"></label>
<label>Tokens / minute <input type="text" name="tpm" value="{{.F.TPM}}" placeholder="none"></label>
<label>Budget <input type="text" name="budget" value="{{.F.Budget}}" placeholder="none"></label>
<label class="q-period">per
<select name="period">
<option value="month"{{if eq .F.Period "month"}} selected{{end}}>month</option>
<option value="day"{{if eq .F.Period "day"}} selected{{end}}>day</option>
</select></label>
</div>
<label>Only these models <span class="opt">comma separated; blank allows all</span>
<input type="text" name="models" value="{{.F.Models}}" placeholder="every model"></label>
<div class="q-form-actions">
<button type="submit">Save</button>
<a href="/admin/quotas">Cancel</a>
<button type="submit" name="clear" value="1" class="link danger"
{{if not .F.HasAny}}disabled{{end}}>Remove these limits</button>
</div>
</form>
{{end}}
{{define "quota-value"}}{{if .Own}}{{.Own}}{{else if .Up}}<span class="q-up" title="set on {{.From}}">{{.Up}} ↑</span>{{else}}<span class="q-none">—</span>{{end}}{{end}}