A user copied the curl from the profile page and sent it as it stood. Every other field in that snippet is filled in for them — the base URL, the key they had just been issued — so "model": "MODEL" reads as a value rather than a blank, and they got: no endpoint serves "MODEL" which is true and useless. It reads the same whether the name was a typo, a placeholder left in from the documentation, or a real outage, and it never says what would have worked. The message now lists what is being served, capped at ten with a count for the rest, and says plainly when nothing is. That list is what /v1/models already returns to the same caller, so it discloses nothing new. The profile page's example now names a model that is actually running, real ones before simulated — the point of an example someone can run is that it runs, and sending a first request to the sim engine would answer in Greek letters. The landing and claim pages keep a placeholder, because they are served without a key and deliberately say nothing about what is here; theirs is <model-name>, which cannot be mistaken for a value, with a line saying where to get the real one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
130 lines
5.7 KiB
HTML
130 lines
5.7 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="/_static/base.css">
|
|
</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="/_static/base.css">
|
|
</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>Your keys · llmgw</title><link rel="stylesheet" href="/_static/base.css">
|
|
</head><body><div class="wrap">
|
|
<header>
|
|
<h1>Your keys</h1>
|
|
<p class="sub">{{.User.Name}} · {{.User.Email}} · project {{.User.ProjectID}}</p>
|
|
<nav>
|
|
<a href="/chat">chat</a>
|
|
<a href="/">about this gateway</a>
|
|
{{if .User.IsAdmin}}<a href="/admin">cluster</a>{{end}}
|
|
<form method="post" action="/logout" class="inline">
|
|
<input type="hidden" name="csrf" value="{{.CSRF}}">
|
|
<button type="submit" class="link">Sign out</button>
|
|
</form>
|
|
</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">
|
|
<input type="hidden" name="csrf" value="{{.CSRF}}">
|
|
<label>What is it for?<input type="text" name="name" placeholder="laptop, ci, notebook" required></label>
|
|
<button type="submit">Create</button>
|
|
</form>
|
|
|
|
<h2>Keys</h2>
|
|
{{if .Keys}}
|
|
<table><tr><th>Name</th><th>Key</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>{{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>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">
|
|
<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>
|
|
|
|
<h2>Your limits</h2>
|
|
{{if .Limits}}
|
|
<table><tr><th>Limit</th><th class="num">Value</th></tr>
|
|
{{range .Limits}}<tr><td>{{.Name}}</td><td class="num">{{.Value}}</td></tr>{{end}}</table>
|
|
{{else}}<p class="empty">No limits are set for you.</p>{{end}}
|
|
|
|
<footer><span>llmgw {{.Version}}</span><span>{{.Now}}</span></footer>
|
|
</div></body></html>{{end}}
|