Files
llmgw/internal/web/templates/landing.html
T
Ilker ManapandClaude Opus 5 0cac9d6986
CI / test (push) Successful in 1m30s
Make the landing page look like something worth having a key for
It is the only page a stranger sees, and until they have a key it is the
whole product: a stack of headings and shell snippets said "internal tool
you probably should not be here".

Now it opens with what this is and a diagram of why it exists — one address,
many GPUs, fabrics that cannot see each other — then the base URL as a
headline rather than a line of prose with a colon in it, the route to a key
as three cards, and the client examples as tabs instead of a pile.

The tabs are radio buttons and a stylesheet. No JavaScript decides what is on
the page, so someone who arrives with scripting off still gets every example;
the only script is the copy buttons, which exist only where the clipboard API
does and so cannot leave a dead control behind.

The page says exactly what it said before, including that it lists no models
and why. The diagram names no model, no node and no count: fabrics are drawn
because llmgw keeps them apart, not because this install has two.

Tests cover the parts that are easy to break later: every example lives in
the page itself rather than being fetched per tab, the reflected base URL —
now in five places rather than one — still cannot write markup, and the
diagram carries a label for anyone not looking at it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 19:14:04 +02:00

215 lines
9.0 KiB
HTML

{{define "landing"}}<!doctype html>
<html lang="en"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>llmgw</title>
<meta name="description" content="One OpenAI-compatible endpoint in front of self-hosted models.">
<link rel="stylesheet" href="/_static/base.css">
</head><body><div class="wrap">
{{if .Notice}}<p class="banner note">{{.Notice}}</p>{{end}}
{{if not .AuthEnabled}}
<p class="banner warn-banner">This gateway has no API keys configured, so it serves anyone
who can reach it. That is a deployment choice, and only safe on a trusted network.</p>
{{end}}
<header class="hero">
<div class="hero-text">
<h1>llmgw</h1>
<p class="sub">One OpenAI-compatible endpoint in front of your own GPUs.</p>
<p class="lede">Point any OpenAI client at this address. The API is the one you
already have a client for — only the base URL changes.</p>
<p class="cta">
{{if .SignupOpen}}<a class="btn" href="/signup">Create an account</a>
<a class="btn ghost" href="/login">Sign in</a>
{{else if .AuthEnabled}}<a class="btn" href="/login">Sign in</a>{{end}}
</p>
</div>
{{/* A schematic of what this is, not of what is installed here. It names no
model, no node and no count: this page is served to anyone who can reach
the address. */}}
<figure class="schematic" aria-labelledby="schematic-cap">
<svg viewBox="0 0 420 200" role="img"
aria-label="Clients reach one endpoint, which places work on GPU nodes across separate fabrics.">
<defs>
<marker id="a" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="6" markerHeight="6" orient="auto">
<path d="M0 0 L8 4 L0 8 z" fill="var(--viz-grid)"/>
</marker>
</defs>
<!-- clients -->
<g font-size="10" fill="var(--muted)">
<rect x="2" y="42" width="84" height="26" rx="6" fill="var(--card)" stroke="var(--line)"/>
<text x="44" y="59" text-anchor="middle" fill="var(--fg)">your client</text>
<rect x="2" y="86" width="84" height="26" rx="6" fill="var(--card)" stroke="var(--line)"/>
<text x="44" y="103" text-anchor="middle" fill="var(--fg)">editor agent</text>
<rect x="2" y="130" width="84" height="26" rx="6" fill="var(--card)" stroke="var(--line)"/>
<text x="44" y="147" text-anchor="middle" fill="var(--fg)">a script</text>
</g>
<g fill="none" stroke="var(--viz-grid)" stroke-width="2" marker-end="url(#a)">
<path d="M88 55 C 118 55 118 92 146 92"/>
<path d="M88 99 h58"/>
<path d="M88 143 C 118 143 118 108 146 108"/>
</g>
<!-- the gateway -->
<rect x="150" y="70" width="72" height="60" rx="8"
fill="var(--accent)" opacity=".12" stroke="var(--accent)" stroke-width="1.5"/>
<text x="186" y="96" text-anchor="middle" font-size="12" font-weight="600"
fill="var(--fg)">llmgw</text>
<text x="186" y="112" text-anchor="middle" font-size="9"
fill="var(--muted)">one address</text>
<g fill="none" stroke="var(--viz-grid)" stroke-width="2" marker-end="url(#a)">
<path d="M224 88 C 254 88 254 46 286 46"/>
<path d="M224 112 C 254 112 254 146 286 146"/>
</g>
<!-- two fabrics that cannot see each other -->
<g font-size="9" fill="var(--muted)">
<rect x="288" y="18" width="128" height="58" rx="8"
fill="none" stroke="var(--line)" stroke-dasharray="4 3"/>
<text x="296" y="32">fabric</text>
<rect x="296" y="40" width="26" height="26" rx="4" fill="var(--viz-1)" opacity=".85"/>
<rect x="330" y="40" width="26" height="26" rx="4" fill="var(--viz-1)" opacity=".85"/>
<rect x="364" y="40" width="26" height="26" rx="4" fill="var(--viz-1)" opacity=".85"/>
<rect x="288" y="118" width="128" height="58" rx="8"
fill="none" stroke="var(--line)" stroke-dasharray="4 3"/>
<text x="296" y="132">fabric</text>
<rect x="296" y="140" width="26" height="26" rx="4" fill="var(--viz-3)" opacity=".85"/>
<rect x="330" y="140" width="26" height="26" rx="4" fill="var(--viz-3)" opacity=".85"/>
<rect x="364" y="140" width="26" height="26" rx="4" fill="var(--viz-3)" opacity=".85"/>
</g>
</svg>
<figcaption id="schematic-cap">Clients see one address. Work is placed on GPUs that
can actually run it — and a model split across machines never straddles two fabrics.</figcaption>
</figure>
</header>
<section class="url-card">
<span class="k">Base URL</span>
<code>{{.BaseURL}}/v1</code>
</section>
<h2>Three steps</h2>
<ol class="steps">
<li>
<span class="n">1</span>
<h3>Get a key</h3>
{{if .SignupOpen}}
<p><a href="/signup">Create an account</a>. Someone approves it, and then you issue
your own keys from your <a href="/profile">profile</a> — as many as you need,
revoking them yourself when a laptop is retired or a script is done.</p>
{{else if .Contact}}
<p>Ask {{if .ContactHref}}<a href="{{.ContactHref}}">{{.Contact}}</a>{{else}}<strong>{{.Contact}}</strong>{{end}}
for one.{{if .AuthEnabled}} If you already have an account,
<a href="/login">sign in</a>.{{end}}</p>
{{else}}
<p>Keys are issued by whoever operates this gateway. If you do not know who that
is, the address you reached this page at is the place to start asking.</p>
{{end}}
<p class="note">A key is shown once and never again — only its hash is kept here —
so put it in a password manager straight away.</p>
</li>
<li>
<span class="n">2</span>
<h3>Point your client at it</h3>
<p>Set the base URL above and your key. Every OpenAI SDK, and every tool built on
one, takes both as configuration.</p>
<pre><code>export OPENAI_BASE_URL={{.BaseURL}}/v1
export OPENAI_API_KEY=$LLMGW_API_KEY</code></pre>
</li>
<li>
<span class="n">3</span>
<h3>Pick a model</h3>
{{if .AuthEnabled}}
<p><code>GET /v1/models</code> lists what is available to your key. It needs the
key, which is why this page does not list them: this page is served to anyone who
can reach the address.</p>
{{else}}
<p><code>GET /v1/models</code> lists what is available.</p>
{{end}}
<p class="note">Sending the placeholder <code>&lt;model-name&gt;</code> unchanged is
answered with the list of models you may use, rather than with an error.</p>
</li>
</ol>
<h2>Connect your client</h2>
<div class="tabs">
<input type="radio" name="client" id="tab-curl" checked><label for="tab-curl">curl</label>
<input type="radio" name="client" id="tab-py"><label for="tab-py">Python</label>
<input type="radio" name="client" id="tab-oc"><label for="tab-oc">OpenCode</label>
<div class="panels">
<pre class="panel"><code>curl {{.BaseURL}}/v1/chat/completions \
-H "Authorization: Bearer $LLMGW_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "&lt;model-name&gt;",
"messages": [{"role": "user", "content": "hello"}]}'</code></pre>
<pre class="panel"><code>import os
from openai import OpenAI
client = OpenAI(base_url="{{.BaseURL}}/v1",
api_key=os.environ["LLMGW_API_KEY"])
reply = client.chat.completions.create(
model="&lt;model-name&gt;",
messages=[{"role": "user", "content": "hello"}],
)
print(reply.choices[0].message.content)</code></pre>
<pre class="panel"><code>// ~/.config/opencode/opencode.json
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"llmgw": {
"npm": "@ai-sdk/openai-compatible",
"name": "llmgw",
"options": {
"baseURL": "{{.BaseURL}}/v1",
"apiKey": "{env:LLMGW_API_KEY}"
},
"models": { "&lt;model-name&gt;": { "name": "&lt;model-name&gt;" } }
}
}
}</code></pre>
</div>
</div>
<p class="note">Anything that speaks the chat-completions API works, whether or not it
is listed here — there is nothing llmgw-specific to install.</p>
<h2>Once you have a key</h2>
<ul class="endpoints">
<li><code>GET {{.BaseURL}}/v1/models</code><span>what your key may use</span></li>
<li><code>GET {{.BaseURL}}/me</code><span>its limits, and what it has spent</span></li>
</ul>
<footer><span>llmgw {{.Version}}</span><span>{{.Now}}</span></footer>
</div>
<script>
// Vanilla and inline, for the same reason this page has no build step. It is an
// enhancement: the button only exists where the clipboard API does, so nothing
// here can leave a dead control on the page.
if (navigator.clipboard) {
for (const pre of document.querySelectorAll('pre')) {
// Read the text before the button joins the element, or "copy" ends up in
// everything anyone pastes.
const text = (pre.querySelector('code') || pre).textContent;
const b = document.createElement('button');
b.type = 'button';
b.className = 'copy';
b.textContent = 'copy';
b.addEventListener('click', () => {
navigator.clipboard.writeText(text).then(() => {
b.textContent = 'copied';
setTimeout(() => { b.textContent = 'copy'; }, 1200);
});
});
pre.appendChild(b);
}
}
</script>
</body></html>{{end}}