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>
196 lines
10 KiB
HTML
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}}
|