The account page (was "Your keys") shows the last thirty days of the person's own use - requests, tokens, spend and tokens per day - and, for each place they can spend, what they used there and a bar for every budget that applies: their own, a per-member one, or a project's, team's or organization's shared with others, named as such. Bars turn amber past four-fifths and red when spent. It sits under the keys now, above the password form. Spend is read from the usage table for the pool each limit counts against, from the start of its period. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
156 lines
7.2 KiB
HTML
156 lines
7.2 KiB
HTML
{{define "signup"}}<!doctype html>
|
|
<html lang="en"><head>
|
|
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<title>Sign up · llmgw</title><link rel="stylesheet" href="{{stylesheet}}">
|
|
</head><body><div class="wrap narrow">
|
|
<header><h1>Create an account</h1>
|
|
<p class="sub">An operator approves it before it can be used.</p></header>
|
|
|
|
{{if .Done}}
|
|
<p>Thank you. Your account is waiting for approval.</p>
|
|
<p class="note">You will not get an email from this gateway — it does not send any.
|
|
Tell whoever you asked for access that you have signed up.</p>
|
|
<p><a href="/login">Sign in</a> once you have been approved.</p>
|
|
{{else}}
|
|
{{if .Error}}<p class="error">{{.Error}}</p>{{end}}
|
|
<form method="post" autocomplete="on">
|
|
<label>Email<input type="email" name="email" value="{{.Email}}" required autocomplete="email"></label>
|
|
<label>Name<input type="text" name="name" value="{{.Name}}" autocomplete="name"></label>
|
|
<label>Password<input type="password" name="password" required minlength="{{.MinPassword}}"
|
|
autocomplete="new-password"></label>
|
|
<p class="note">At least {{.MinPassword}} characters. Length is the only rule —
|
|
a long phrase beats a short one with punctuation in it.</p>
|
|
<button type="submit">Sign up</button>
|
|
</form>
|
|
<p class="note">Already have an account? <a href="/login">Sign in</a>.</p>
|
|
{{end}}
|
|
<footer><span>llmgw {{.Version}}</span><span><a href="/">about this gateway</a></span></footer>
|
|
</div></body></html>{{end}}
|
|
|
|
{{define "login"}}<!doctype html>
|
|
<html lang="en"><head>
|
|
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<title>Sign in · llmgw</title><link rel="stylesheet" href="{{stylesheet}}">
|
|
</head><body><div class="wrap narrow">
|
|
<header><h1>Sign in</h1></header>
|
|
|
|
{{if .Error}}<p class="error">{{.Error}}</p>{{end}}
|
|
<form method="post" autocomplete="on">
|
|
<label>Email<input type="email" name="email" value="{{.Email}}" required autocomplete="email"></label>
|
|
<label>Password<input type="password" name="password" required autocomplete="current-password"></label>
|
|
<button type="submit">Sign in</button>
|
|
</form>
|
|
{{if .SignupOpen}}<p class="note">No account? <a href="/signup">Sign up</a>.</p>{{end}}
|
|
<footer><span>llmgw {{.Version}}</span><span><a href="/">about this gateway</a></span></footer>
|
|
</div></body></html>{{end}}
|
|
|
|
{{define "profile"}}<!doctype html>
|
|
<html lang="en"><head>
|
|
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<title>Account · llmgw</title><link rel="stylesheet" href="{{stylesheet}}">
|
|
</head><body><div class="wrap">
|
|
<header>
|
|
<h1>Your account</h1>
|
|
<p class="sub">{{.User.Email}}{{if ne .User.Name .User.Email}} ({{.User.Name}}){{end}}</p>
|
|
{{template "nav" .Nav}}
|
|
</header>
|
|
|
|
{{if .Error}}<p class="error">{{.Error}}</p>{{end}}
|
|
|
|
{{if .NewKey}}
|
|
<h2>Your new key</h2>
|
|
<p>This is shown once. Nothing can produce it again — only its hash is kept.</p>
|
|
<pre>{{.NewKey}}</pre>
|
|
<p>Put it in a password manager now.</p>
|
|
<pre>curl {{.BaseURL}}/v1/chat/completions \
|
|
-H "Authorization: Bearer {{.NewKey}}" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"model": "{{.ExampleModel}}", "messages": [{"role":"user","content":"hello"}]}'</pre>
|
|
{{end}}
|
|
|
|
<h2>Create a key</h2>
|
|
<form method="post" action="/profile/keys" class="narrow">
|
|
<input type="hidden" name="csrf" value="{{.CSRF}}">
|
|
<label>What is it for?<input type="text" name="name" placeholder="laptop, ci, notebook" required></label>
|
|
{{if .Contexts}}
|
|
<label>Spends against
|
|
<select name="context">
|
|
{{range .Contexts}}<option value="{{.Value}}">{{.Label}}</option>{{end}}
|
|
</select></label>
|
|
<button type="submit">Create</button>
|
|
{{else}}
|
|
<p class="note">You are in no project yet, and personal keys are off here.
|
|
Ask an administrator to add you to one.</p>
|
|
{{end}}
|
|
</form>
|
|
|
|
<h2>Keys</h2>
|
|
{{if .Keys}}
|
|
<table><tr><th>Name</th><th>Key</th><th>Spends against</th><th>Created</th><th>Last used</th><th>State</th><th></th></tr>
|
|
{{range .Keys}}<tr>
|
|
<td>{{.Name}}</td>
|
|
<td><code>{{.Prefix}}</code></td>
|
|
<td>{{if .ProjectID}}{{or (index $.Where .ProjectID) "a project you have left"}}{{else}}Personal{{end}}</td>
|
|
<td>{{date .CreatedAt}}</td>
|
|
<td>{{ago .LastUsedAt}}</td>
|
|
<td class="state state-{{state .}}">{{state .}}</td>
|
|
<td>{{if eq (state .) "active"}}
|
|
<form method="post" action="/profile/keys/{{.ID}}/revoke">
|
|
<input type="hidden" name="csrf" value="{{$.CSRF}}">
|
|
<button type="submit" class="link danger">Revoke</button>
|
|
</form>
|
|
{{end}}</td>
|
|
</tr>{{end}}</table>
|
|
<p class="note">Revoking takes effect within the gateway's cache window, up to a
|
|
minute. To rotate: create the new key, switch over, wait for "last used" on the
|
|
old one to stop moving, then revoke it.</p>
|
|
{{else}}<p class="empty">You have no keys yet.</p>{{end}}
|
|
|
|
<h2>Where you are</h2>
|
|
{{if .Memberships}}
|
|
<p>{{range $i, $m := .Memberships}}{{if $i}} · {{end}}{{$m.Path}} <span class="opt">{{$m.Scope}}</span>{{end}}</p>
|
|
{{else}}<p class="empty">You are not a member of any organization, team or project.</p>{{end}}
|
|
|
|
<h2>Your use and limits</h2>
|
|
{{with .Own}}
|
|
<div class="tiles own-use">
|
|
<div class="tile"><div class="v">{{compact .Requests}}</div><div class="k">requests · 30 days</div></div>
|
|
<div class="tile"><div class="v">{{compact .Tokens}}</div><div class="k">tokens · 30 days</div></div>
|
|
<div class="tile"><div class="v">{{.Spend}}</div><div class="k">spend · 30 days</div></div>
|
|
</div>
|
|
{{with .Chart}}<div class="chart-wrap"><h3 class="chart-title">Tokens per day</h3>{{.}}</div>{{end}}
|
|
{{end}}
|
|
{{range .LimitsBy}}
|
|
<h3 class="limits-for">{{.Label}}{{with .Use}} <span class="opt">— {{compact .Requests}} requests, {{compact .Tokens}} tokens in 30 days</span>{{end}}</h3>
|
|
{{range .Budgets}}
|
|
<div class="budget{{if .Over}} over{{else if .Near}} near{{end}}">
|
|
<div class="budget-head"><span>{{.Label}}</span><span><b>{{.Used}}</b> of {{.Budget}} {{.Period}}</span></div>
|
|
<div class="budget-bar"><span style="width: {{.Percent}}%"></span></div>
|
|
</div>
|
|
{{end}}
|
|
{{if .Rows}}
|
|
<table class="compact narrow-table"><tr><th>Limit</th><th class="num">Value</th></tr>
|
|
{{range .Rows}}<tr><td>{{.Name}}</td><td class="num">{{.Value}}</td></tr>{{end}}</table>
|
|
{{else}}<p class="empty">No limits here.</p>{{end}}
|
|
{{else}}<p class="empty">Nowhere to spend yet.</p>{{end}}
|
|
<p class="note">Everything that applies, from the organization down to you: the
|
|
strictest value wins.</p>
|
|
|
|
<h2>Password</h2>
|
|
{{if .PasswordChanged}}<p>Your password was changed. Any other browser you were
|
|
signed in on has been signed out.</p>{{end}}
|
|
{{if .PasswordError}}<p class="error">{{.PasswordError}}</p>{{end}}
|
|
<form method="post" action="/profile/password" autocomplete="on" class="narrow">
|
|
<input type="hidden" name="csrf" value="{{.CSRF}}">
|
|
<label>Current password<input type="password" name="current" required
|
|
autocomplete="current-password"></label>
|
|
<label>New password<input type="password" name="new" required
|
|
minlength="{{.MinPassword}}" autocomplete="new-password"></label>
|
|
<button type="submit">Change password</button>
|
|
</form>
|
|
<p class="note">Changing it signs out every other browser, which is the point if
|
|
you are changing it because something went wrong.</p>
|
|
|
|
<footer><span>llmgw {{.Version}}</span><span>{{.Now}}</span></footer>
|
|
</div></body></html>{{end}}
|