Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
202 lines
8.0 KiB
HTML
202 lines
8.0 KiB
HTML
{{define "landing"}}<!doctype html>
|
|
<html lang="{{lang}}"><head>
|
|
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<title>llmgw</title>
|
|
<meta name="description" content="{{t "landing.description"}}">
|
|
<link rel="stylesheet" href="{{stylesheet}}">
|
|
</head><body><div class="wrap">
|
|
|
|
{{if .Notice}}<p class="banner note">{{.Notice}}</p>{{end}}
|
|
{{if not .AuthEnabled}}
|
|
<p class="banner warn-banner">{{t "landing.open_warning"}}</p>
|
|
{{end}}
|
|
|
|
<header class="hero">
|
|
<div class="hero-text">
|
|
<h1>llmgw</h1>
|
|
<p class="sub">{{t "landing.sub"}}</p>
|
|
<p class="lede">{{t "landing.lede"}}</p>
|
|
<p class="cta">
|
|
{{if .SignupOpen}}<a class="btn" href="/signup">{{t "landing.create_account"}}</a>
|
|
<a class="btn ghost" href="/login">{{t "landing.sign_in"}}</a>
|
|
{{else if .AuthEnabled}}<a class="btn" href="/login">{{t "landing.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="{{t "landing.schematic"}}">
|
|
<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)">{{t "landing.schematic.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)">{{t "landing.schematic.editor"}}</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)">{{t "landing.schematic.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)" translate="no">llmgw</text>
|
|
<text x="186" y="112" text-anchor="middle" font-size="9"
|
|
fill="var(--muted)">{{t "landing.schematic.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">{{t "landing.schematic.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">{{t "landing.schematic.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">{{t "landing.schematic_caption"}}</figcaption>
|
|
</figure>
|
|
</header>
|
|
|
|
<section class="url-card">
|
|
<span class="k">{{t "landing.base_url"}}</span>
|
|
<code>{{.BaseURL}}/v1</code>
|
|
</section>
|
|
|
|
<h2>{{t "landing.three_steps"}}</h2>
|
|
<ol class="steps">
|
|
<li>
|
|
<span class="n">1</span>
|
|
<h3>{{t "landing.step1"}}</h3>
|
|
{{if .SignupOpen}}
|
|
<p>{{th "landing.step1_signup"}}</p>
|
|
{{else if .Contact}}
|
|
<p>{{th "landing.step1_contact" .ContactHTML}}{{if .AuthEnabled}}{{th "landing.step1_contact_signin"}}{{end}}</p>
|
|
{{else}}
|
|
<p>{{t "landing.step1_nobody"}}</p>
|
|
{{end}}
|
|
<p class="note">{{t "landing.step1_shown_once"}}</p>
|
|
</li>
|
|
<li>
|
|
<span class="n">2</span>
|
|
<h3>{{t "landing.step2"}}</h3>
|
|
<p>{{t "landing.step2_text"}}</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>{{t "landing.step3"}}</h3>
|
|
{{if .AuthEnabled}}
|
|
<p>{{th "landing.step3_keyed"}}</p>
|
|
{{else}}
|
|
<p>{{th "landing.step3_open"}}</p>
|
|
{{end}}
|
|
<p class="note">{{th "landing.step3_placeholder"}}</p>
|
|
</li>
|
|
</ol>
|
|
|
|
<h2>{{t "landing.connect"}}</h2>
|
|
<div class="tabs">
|
|
<input type="radio" name="client" id="tab-curl" checked><label for="tab-curl" translate="no">curl</label>
|
|
<input type="radio" name="client" id="tab-py"><label for="tab-py" translate="no">Python</label>
|
|
<input type="radio" name="client" id="tab-oc"><label for="tab-oc" translate="no">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": "<model-name>",
|
|
"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="<model-name>",
|
|
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": { "<model-name>": { "name": "<model-name>" } }
|
|
}
|
|
}
|
|
}</code></pre>
|
|
</div>
|
|
</div>
|
|
<p class="note">{{t "landing.anything_works"}} {{th "landing.client_guide"}}</p>
|
|
|
|
<h2>{{t "landing.once_you_have_key"}}</h2>
|
|
<ul class="endpoints">
|
|
<li><code>GET {{.BaseURL}}/v1/models</code><span>{{t "landing.models_endpoint"}}</span></li>
|
|
<li><code>GET {{.BaseURL}}/me</code><span>{{t "landing.me_endpoint"}}</span></li>
|
|
</ul>
|
|
|
|
<footer><span><a href="/about">llmgw {{.Version}}</a></span><span>{{.Now}}</span>{{template "langs"}}</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.
|
|
const copyWord = {{t "common.copy"}}, copiedWord = {{t "common.copied"}};
|
|
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 = copyWord;
|
|
b.addEventListener('click', () => {
|
|
navigator.clipboard.writeText(text).then(() => {
|
|
b.textContent = copiedWord;
|
|
setTimeout(() => { b.textContent = copyWord; }, 1200);
|
|
});
|
|
});
|
|
pre.appendChild(b);
|
|
}
|
|
}
|
|
</script>
|
|
</body></html>{{end}}
|