The install guide shows every screen of a real install
/install takes a reader from three bare Ubuntu machines to a first answer: what each machine needs and the commands that check it, the release fetched and its checksum checked, the eight setup screens, the system's state and the console, and a first request with a key from the account page. Every screen is from an install of v0.14.9 on three fresh VMs, recorded with tmux and kept as terminal frames: the page shows them as text, and plays a session back when asked, fetching it compressed from /install/cast/. The administrator's password and the API key were masked before the recording was kept, and a test fails if either shape reaches the page. /about gains what llmgw now does beyond routing, and its install steps are today's: the setup screens and node add, not onboard-node.sh and a model file, with a link into the guide. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f8d18efaef
commit
fea5774fe5
18 files changed
+1225
-219
No files matched your search
@@ -9,3 +9,5 @@ coverage.out
|
||||
/terraform-provider-llmgw/terraform-provider-llmgw
|
||||
/terraform-provider-llmgw/examples/.terraform*
|
||||
/terraform-provider-llmgw/examples/terraform.tfstate*
|
||||
# What a development server run from the checkout writes beside it.
|
||||
/secrets/
|
||||
@@ -28,6 +28,16 @@ APIs change between minors without a deprecation period.
|
||||
|
||||
### Added
|
||||
|
||||
- **An install guide with every screen, at `/install`.** From three bare
|
||||
Ubuntu machines to a first answer: what each machine needs and how to
|
||||
check it, the release fetched and checked, the setup screens one by one,
|
||||
the system's state, and a first request with a key from the account page.
|
||||
The screens are a real three-machine install, recorded and kept as
|
||||
terminal frames — sharp at any size and a few kilobytes — with each
|
||||
session played back on request; the administrator's password and every
|
||||
key were masked before it was kept. `/about` says more of what llmgw does
|
||||
(documents, guardrails, sign-in, high availability, pictures and 3D), and
|
||||
its install steps are the ones of today, leading to the guide.
|
||||
- **The company's MCP tools under the guardrails, a rate and a breaker.**
|
||||
- **The answer:** the caller's policies read a tool's answer as the prompt it becomes. An instruction planted in a
|
||||
ticket is withheld as a pasted one would be, and personal data is redacted.
|
||||
|
||||
@@ -204,6 +204,9 @@ func (r *Router) Handler() http.Handler {
|
||||
// What llmgw is and how to install it: about the software, not this
|
||||
// installation, so it needs no key either.
|
||||
mux.HandleFunc("GET /about", web.About(version.Version))
|
||||
// The install guide, recorded on a real install: as public as /about.
|
||||
mux.HandleFunc("GET /install", web.Install(version.Version))
|
||||
mux.HandleFunc("GET /install/cast/{file}", web.InstallCast)
|
||||
mux.Handle("GET /_static/", web.Static())
|
||||
// The language switcher at the foot of every page, which has to work
|
||||
// before anyone has signed in.
|
||||
@@ -366,7 +369,7 @@ func (r *Router) Handler() http.Handler {
|
||||
r.selfHandler.Store(&h)
|
||||
return h
|
||||
}
|
||||
exempt := []string{"/health", "/ready", "/leader", "/v1/openapi.json", "/", "/about", "/_static/", "/_lang/", "/claim/",
|
||||
exempt := []string{"/health", "/ready", "/leader", "/v1/openapi.json", "/", "/about", "/install", "/install/", "/_static/", "/_lang/", "/claim/",
|
||||
"/signup", "/login", "/login/", "/logout", "/profile", "/profile/", "/scim/",
|
||||
"/admin", "/admin/", "/chat", "/chat/", "/images", "/images/", "/3d", "/3d/", "/org", "/org/", "/setup/"}
|
||||
// The docs are public unless the site said otherwise, and then guarded by
|
||||
|
||||
@@ -2,6 +2,7 @@ package web
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"html/template"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
@@ -40,6 +41,20 @@ type AboutData struct {
|
||||
Site bool
|
||||
}
|
||||
|
||||
// InstallURL is the install guide: on a gateway its /install, on the website
|
||||
// the page beside this one.
|
||||
func (d AboutData) InstallURL() string {
|
||||
if d.Site {
|
||||
return "install.html"
|
||||
}
|
||||
return "/install"
|
||||
}
|
||||
|
||||
// Still is a screen of the install guide's recording, for the steps here.
|
||||
func (d AboutData) Still(name string) template.HTML {
|
||||
return InstallData{Site: d.Site}.Still(name)
|
||||
}
|
||||
|
||||
// releaseTag is a version the releases page has files for.
|
||||
var releaseTag = regexp.MustCompile(`^v\d+\.\d+\.\d+$`)
|
||||
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,249 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"crypto/sha256"
|
||||
_ "embed"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"html/template"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// The install guide: every step from bare machines to a first answer, each
|
||||
// shown as the screen it was on. The screens are a real install, recorded on
|
||||
// three fresh Ubuntu machines, kept as terminal frames rather than pictures:
|
||||
// crisp at any size, a few kilobytes, text a reader can select — and played
|
||||
// back, where the reader asks for it, as the session it was.
|
||||
//
|
||||
// The recording is guide/guide.json.gz, made by the scripts that drove the
|
||||
// install; the administrator's password and every key in it were masked
|
||||
// before it was written. Like /about, the page says nothing about this
|
||||
// installation, only about the software, and is served without a key.
|
||||
|
||||
//go:embed guide/guide.json.gz
|
||||
var guideGz []byte
|
||||
|
||||
// guideCast is one recorded session: its frames, and its last screen to show
|
||||
// before anyone presses play.
|
||||
type guideCast struct {
|
||||
Cols int `json:"cols"`
|
||||
Rows int `json:"rows"`
|
||||
Frames []json.RawMessage `json:"frames"`
|
||||
Poster []string `json:"poster"`
|
||||
}
|
||||
|
||||
// guideStill is one screen of a session, picked for the step it shows.
|
||||
type guideStill struct {
|
||||
Cast string `json:"cast"`
|
||||
Frame int `json:"frame"`
|
||||
Lines []string `json:"lines"`
|
||||
}
|
||||
|
||||
type guideFile struct {
|
||||
Casts map[string]guideCast `json:"casts"`
|
||||
Stills map[string]guideStill `json:"stills"`
|
||||
}
|
||||
|
||||
// guide is read once, when the binary starts: a recording that does not
|
||||
// parse is a build that should not ship, and the tests say so first.
|
||||
var guide = func() guideFile {
|
||||
zr, err := gzip.NewReader(bytes.NewReader(guideGz))
|
||||
if err != nil {
|
||||
panic("web: the install guide's recording is damaged: " + err.Error())
|
||||
}
|
||||
b, err := io.ReadAll(zr)
|
||||
if err != nil {
|
||||
panic("web: the install guide's recording is damaged: " + err.Error())
|
||||
}
|
||||
var g guideFile
|
||||
if err := json.Unmarshal(b, &g); err != nil {
|
||||
panic("web: the install guide's recording does not parse: " + err.Error())
|
||||
}
|
||||
return g
|
||||
}()
|
||||
|
||||
// castFiles are the recordings as the player fetches them, compressed once:
|
||||
// the page itself carries only each one's last screen, and a session is
|
||||
// downloaded when someone presses play.
|
||||
var castFiles = func() map[string][]byte {
|
||||
out := map[string][]byte{}
|
||||
for name, c := range guide.Casts {
|
||||
b, err := json.Marshal(struct {
|
||||
Rows int `json:"rows"`
|
||||
Frames []json.RawMessage `json:"frames"`
|
||||
}{c.Rows, c.Frames})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
var z bytes.Buffer
|
||||
zw, _ := gzip.NewWriterLevel(&z, gzip.BestCompression)
|
||||
_, _ = zw.Write(b)
|
||||
_ = zw.Close()
|
||||
out[name] = z.Bytes()
|
||||
}
|
||||
return out
|
||||
}()
|
||||
|
||||
// CastView is what the page needs of a recording: the screen it opens on,
|
||||
// and where the player fetches the whole session.
|
||||
type CastView struct {
|
||||
Name string
|
||||
Cols, Rows int
|
||||
Poster template.HTML
|
||||
Src string
|
||||
}
|
||||
|
||||
// InstallData is what the guide is rendered with.
|
||||
type InstallData struct {
|
||||
Version, Release string
|
||||
Site bool
|
||||
}
|
||||
|
||||
// recorded is when the guide's install was made, and with which release: the
|
||||
// screens are that release's, whatever version serves the page.
|
||||
const recorded = "v0.14.9 · 2026-10-23"
|
||||
|
||||
// Recorded says which release the screens are of, and when they were taken.
|
||||
func (InstallData) Recorded() string { return recorded }
|
||||
|
||||
// AboutURL is the page about llmgw: on a gateway its /about, on the website
|
||||
// the site's front page beside this one.
|
||||
func (d InstallData) AboutURL() string {
|
||||
if d.Site {
|
||||
return "./"
|
||||
}
|
||||
return "/about"
|
||||
}
|
||||
|
||||
// Img is a picture of the portal, served by the gateway or, on the website,
|
||||
// beside the page.
|
||||
func (d InstallData) Img(name string) string {
|
||||
if d.Site {
|
||||
return "guide/" + name
|
||||
}
|
||||
return assetURL("guide/" + name)
|
||||
}
|
||||
|
||||
// ShotView is one setup screen and the label over it.
|
||||
type ShotView struct {
|
||||
Label string
|
||||
Screen template.HTML
|
||||
}
|
||||
|
||||
// Shot is the setup screen called name, labelled for its step.
|
||||
func (d InstallData) Shot(name, label string) ShotView {
|
||||
return ShotView{Label: label, Screen: d.Still(name)}
|
||||
}
|
||||
|
||||
// Cast is the recording called name, ready for the page. On a gateway it is
|
||||
// fetched from /install/cast/, named by its content so a cache never plays an
|
||||
// old one; on the website, from beside the page.
|
||||
func (d InstallData) Cast(name string) CastView {
|
||||
c, ok := guide.Casts[name]
|
||||
if !ok {
|
||||
panic("web: the install guide has no recording " + name)
|
||||
}
|
||||
v := CastView{Name: name, Cols: c.Cols, Rows: c.Rows, Poster: screen(c.Poster)}
|
||||
if d.Site {
|
||||
v.Src = "guide/" + name + ".json"
|
||||
} else {
|
||||
sum := sha256.Sum256(castFiles[name])
|
||||
v.Src = "/install/cast/" + name + ".json?v=" + hex.EncodeToString(sum[:4])
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// CastFile is a recording as the website serves it beside the page: JSON,
|
||||
// compressed, for the website's build to write out under guide/.
|
||||
func CastFile(name string) ([]byte, bool) {
|
||||
b, ok := castFiles[name]
|
||||
return b, ok
|
||||
}
|
||||
|
||||
// CastNames are the recordings the guide plays.
|
||||
func CastNames() []string {
|
||||
var names []string
|
||||
for n := range guide.Casts {
|
||||
names = append(names, n)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// InstallCast serves one recording at /install/cast/{name}.json, compressed
|
||||
// when the browser takes it so, as nearly every one does.
|
||||
func InstallCast(w http.ResponseWriter, r *http.Request) {
|
||||
name := strings.TrimSuffix(r.PathValue("file"), ".json")
|
||||
b, ok := castFiles[name]
|
||||
if !ok || !strings.HasSuffix(r.PathValue("file"), ".json") {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Cache-Control", "public, max-age=86400")
|
||||
w.Header().Set("Vary", "Accept-Encoding")
|
||||
if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
||||
w.Header().Set("Content-Encoding", "gzip")
|
||||
_, _ = w.Write(b)
|
||||
return
|
||||
}
|
||||
zr, err := gzip.NewReader(bytes.NewReader(b))
|
||||
if err != nil {
|
||||
http.Error(w, "the recording is damaged", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
_, _ = io.Copy(w, zr)
|
||||
}
|
||||
|
||||
// Still is the screen of a step, as the recording showed it.
|
||||
func (InstallData) Still(name string) template.HTML {
|
||||
s, ok := guide.Stills[name]
|
||||
if !ok {
|
||||
panic("web: the install guide has no screen " + name)
|
||||
}
|
||||
return screen(s.Lines)
|
||||
}
|
||||
|
||||
// screen joins a frame's lines. They are HTML the recording's builder made
|
||||
// from the terminal's own colours — spans and escaped text, nothing a visitor
|
||||
// supplied — which is why they are trusted here.
|
||||
//
|
||||
// Blank rows at the foot are left off: a shell session that filled a third
|
||||
// of the terminal would otherwise sit above a block of black. The player
|
||||
// gives the screen its full height once it plays.
|
||||
func screen(lines []string) template.HTML {
|
||||
n := len(lines)
|
||||
for n > 0 && strings.TrimSpace(tagText.ReplaceAllString(lines[n-1], "")) == "" {
|
||||
n--
|
||||
}
|
||||
return template.HTML(strings.Join(lines[:n], "\n"))
|
||||
}
|
||||
|
||||
// tagText matches the markup in a screen's line, to see whether any text is left.
|
||||
var tagText = regexp.MustCompile(`<[^>]*>`)
|
||||
|
||||
// NewInstallData fills the guide's data for a version.
|
||||
func NewInstallData(version string) InstallData {
|
||||
d := InstallData{Version: version}
|
||||
if releaseTag.MatchString(version) {
|
||||
d.Release = version
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
// RenderInstall writes the guide, for the website's build as much as for Install.
|
||||
func RenderInstall(w io.Writer, d InstallData) error {
|
||||
return tmpl.ExecuteTemplate(w, "install", d)
|
||||
}
|
||||
|
||||
// Install serves the guide at /install.
|
||||
func Install(version string) http.HandlerFunc {
|
||||
d := NewInstallData(version)
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
render(w, r, "install", d)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestInstallGuideShowsEveryStepAndRecording(t *testing.T) {
|
||||
code, body := get(t, Install("v1.2.3"), "http://llmgw.example/install", nil)
|
||||
if code != http.StatusOK {
|
||||
t.Fatalf("status %d", code)
|
||||
}
|
||||
for _, want := range []string{
|
||||
"<!doctype html>",
|
||||
`id="before"`, `id="binary"`, `id="setup"`, `id="check"`, `id="use"`, `id="trouble"`,
|
||||
"sha256sum -c --ignore-missing",
|
||||
"sudo llmgw server init",
|
||||
`href="/about"`,
|
||||
"/_static/guide/admin.png?v=",
|
||||
// The recording's own screens, not a description of them.
|
||||
"High availability", "All three are ready.", "The control plane is up",
|
||||
} {
|
||||
if !strings.Contains(body, want) {
|
||||
t.Errorf("the guide does not have %q", want)
|
||||
}
|
||||
}
|
||||
if n := strings.Count(body, `class="tty" data-src="/install/cast/`); n != len(guide.Casts) {
|
||||
t.Errorf("%d players on the page, %d recordings", n, len(guide.Casts))
|
||||
}
|
||||
}
|
||||
|
||||
// The recording was of a real install, with a real administrator's password
|
||||
// and a real key on screen. Neither may reach the page.
|
||||
func TestInstallGuideCarriesNoSecret(t *testing.T) {
|
||||
_, body := get(t, Install("v1.2.3"), "http://llmgw.example/install", nil)
|
||||
for _, rx := range []*regexp.Regexp{
|
||||
regexp.MustCompile(`sk-llmgw-[A-Za-z0-9_-]{8,}`),
|
||||
regexp.MustCompile(`password(\s|<[^>]*>)+[a-z0-9]{16,}`),
|
||||
} {
|
||||
if m := rx.FindString(body); m != "" {
|
||||
t.Errorf("the guide shows %q", m)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEveryGuideStepHasItsScreen(t *testing.T) {
|
||||
for _, name := range []string{"setup-start", "setup-choices", "setup-admin", "setup-machines",
|
||||
"setup-checks", "setup-plan", "setup-install", "setup-done"} {
|
||||
s, ok := guide.Stills[name]
|
||||
if !ok || len(s.Lines) == 0 {
|
||||
t.Errorf("no screen %s", name)
|
||||
continue
|
||||
}
|
||||
if _, ok := guide.Casts[s.Cast]; !ok {
|
||||
t.Errorf("screen %s is of a recording, %s, the guide does not have", name, s.Cast)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstallGuideAsTheWebsite(t *testing.T) {
|
||||
d := NewInstallData("v1.2.3")
|
||||
d.Site = true
|
||||
var b bytes.Buffer
|
||||
if err := RenderInstall(&b, d); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
body := b.String()
|
||||
for _, not := range []string{"<!doctype", "</body>", "/_static/", `href="/about"`} {
|
||||
if strings.Contains(body, not) {
|
||||
t.Errorf("the website's guide has %q", not)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(body, `src="guide/admin.png"`) {
|
||||
t.Error("the website's guide does not take its pictures from beside it")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTheRecordingsAreServedCompressed(t *testing.T) {
|
||||
r := httptest.NewRequest("GET", "http://llmgw.example/install/cast/2-setup.json", nil)
|
||||
r.SetPathValue("file", "2-setup.json")
|
||||
r.Header.Set("Accept-Encoding", "gzip, br")
|
||||
w := httptest.NewRecorder()
|
||||
InstallCast(w, r)
|
||||
if w.Code != http.StatusOK || w.Header().Get("Content-Encoding") != "gzip" {
|
||||
t.Fatalf("%d %v", w.Code, w.Header())
|
||||
}
|
||||
// And plain to a client that does not take gzip.
|
||||
r = httptest.NewRequest("GET", "http://llmgw.example/install/cast/2-setup.json", nil)
|
||||
r.SetPathValue("file", "2-setup.json")
|
||||
w = httptest.NewRecorder()
|
||||
InstallCast(w, r)
|
||||
if !strings.HasPrefix(w.Body.String(), `{"rows":`) {
|
||||
t.Errorf("plain body starts %.20q", w.Body.String())
|
||||
}
|
||||
r = httptest.NewRequest("GET", "http://llmgw.example/install/cast/nothing.json", nil)
|
||||
r.SetPathValue("file", "nothing.json")
|
||||
w = httptest.NewRecorder()
|
||||
InstallCast(w, r)
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Errorf("an unknown recording answered %d", w.Code)
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,6 @@
|
||||
"about.nav.download": "Download",
|
||||
"about.nav.source": "Source",
|
||||
"about.nav.gateway": "This gateway",
|
||||
|
||||
"about.hero.eyebrow": "Self-hosted LLM gateway · %s",
|
||||
"about.hero.title": "One endpoint in front of <em>your own GPUs</em>.",
|
||||
"about.hero.lede": "llmgw runs vLLM, SGLang and llama.cpp across your servers and serves them behind one OpenAI-compatible API. It knows which GPUs share an NVLink domain or an InfiniBand fabric, and a model split across machines never straddles two.",
|
||||
@@ -22,7 +21,6 @@
|
||||
"about.schem.title": "Clients reach one llmgw address. llmgw places a model's shards on GPUs inside one fabric at a time.",
|
||||
"about.schem.editor": "editor agent",
|
||||
"about.schem.caption": "A replica spanning nodes goes into one NVLink domain if one has room, otherwise onto one InfiniBand fabric. Two fabrics that cannot see each other are never bridged: the all-reduce would hang.",
|
||||
|
||||
"about.what.title": "What it does",
|
||||
"about.what.intro": "One static binary. It runs as the control plane, as the agent on every GPU node, and as the weights mirror.",
|
||||
"about.what.api.title": "OpenAI API, your hardware",
|
||||
@@ -34,7 +32,7 @@
|
||||
"about.what.keys.title": "Keys, budgets, rate limits",
|
||||
"about.what.keys.text": "Organisations, teams and projects, each with limits. Priority bands decide who waits when the cluster is full. Managers see their organisation's spend; people see their own.",
|
||||
"about.what.editors.title": "Editors connect from the account page",
|
||||
"about.what.editors.text": "Pick Continue, Codex or Cline and a model. The page makes a key and hands over the config, filled in, or a one-line command that writes it.",
|
||||
"about.what.editors.text": "Continue, Codex or Cline from the account page: a key, and the configuration filled in or a one-line command that writes it. Claude Code and OpenCode work with the same key, over the Anthropic and OpenAI APIs.",
|
||||
"about.what.chat.title": "A chat page for quick questions",
|
||||
"about.what.chat.text": "Markdown and maths rendered, stop mid-reply, tokens per second and time to first token, a system prompt and a temperature. The gateway keeps no conversations.",
|
||||
"about.what.mirror.title": "A mirror you browse",
|
||||
@@ -45,7 +43,6 @@
|
||||
"about.what.upgrades.text": "Upgrade the control plane, then the agents. A restarted agent adopts the engines it was running instead of reloading them, so replicas stay up throughout.",
|
||||
"about.what.dashboard.title": "A dashboard with nothing to set up",
|
||||
"about.what.dashboard.text": "<code>/admin</code> shows capacity per GPU class, CPU and memory per node, who is using what, each node's page and logs. It fits a phone. Prometheus is optional.",
|
||||
|
||||
"about.fabrics.title": "InfiniBand and NVLink, several of each",
|
||||
"about.fabrics.intro": "A site may have independent InfiniBand fabrics, eight-GPU HGX boards and NVL72 racks side by side. llmgw tells them apart by what the adapters report, not by what a config file calls them.",
|
||||
"about.fabrics.guide": "Fabrics and NVLink guide",
|
||||
@@ -66,20 +63,18 @@
|
||||
"about.fabrics.racks.text": "<code>/admin/nvlink</code> shows which trays are joined, whose IMEX is down, which link runs degraded.",
|
||||
"about.fabrics.ports.title": "Ports re-read every heartbeat",
|
||||
"about.fabrics.ports.text": "A port that goes down takes its fabric off the node within seconds. Counters and rates are exported to Prometheus.",
|
||||
|
||||
"about.install.title": "From nothing to a first request",
|
||||
"about.install.intro": "Debian, Ubuntu or RHEL 10. NVIDIA drivers and the InfiniBand stack are yours to install; llmgw does the rest.",
|
||||
"about.install.guide": "<a href=\"https://gitea.mynodes.xyz/ilker/llmgw/src/branch/main/docs/install.md\">The full guide</a> covers the mirror, GPUs and what to do when something does not work.",
|
||||
"about.install.intro": "Three machines for high availability, or one. Ubuntu, Debian or RHEL 10; llmgw installs its engines, Python and CUDA itself.",
|
||||
"about.install.guide": "<a href=\"%s\">The install guide</a> shows every step on the screens of a real install, recorded.",
|
||||
"about.install.binary.title": "Put the binary on every machine",
|
||||
"about.install.binary.text": "Static, no dependencies, the same file everywhere.",
|
||||
"about.install.server.title": "Set up the control plane",
|
||||
"about.install.server.text": "Creates the user, the certificate authority, the certificate, <code>server.yaml</code> and your administrator account, then starts the service. Safe to run again. Add <code>--host</code> for any name nodes use that the machine cannot see itself.",
|
||||
"about.install.node.title": "Add a node",
|
||||
"about.install.node.text": "From the control plane, which holds the authority. The node gets its certificate, the binary and the agent, and installs its engine by itself.",
|
||||
"about.install.server.title": "Run the setup screens",
|
||||
"about.install.server.text": "With nothing installed, <code>sudo llmgw</code> opens the setup: one machine or three, what to install, the other machines' logins. It checks every machine first, shows the plan, and installs. From a script, <code>llmgw server init</code> does the same.",
|
||||
"about.install.node.title": "Add GPU nodes",
|
||||
"about.install.node.text": "From the control plane, over SSH. The node gets llmgw, is checked by <code>llmgw doctor</code>, receives its certificate and starts its agent; it installs its engines by itself.",
|
||||
"about.install.node.output_label": "Example output of llmgw ctl nodes",
|
||||
"about.install.model.title": "Add a model and send a request",
|
||||
"about.install.model.text": "No restart. If it does not place, <code>llmgw ctl explain-placement</code> names every node and why it was turned down.",
|
||||
|
||||
"about.install.model.text": "From <code>/admin/mirror</code>, what your nodes can run, downloaded once for the cluster; or by name. If it does not place, <code>llmgw ctl explain-placement</code> says why.",
|
||||
"about.proxy.title": "Behind a reverse proxy",
|
||||
"about.proxy.intro": "Port 8080 is plain HTTP. Put it behind Caddy or nginx with a public certificate. Only 8080 goes through the proxy: agents reach 7443 directly, because it is mutual TLS with the cluster's own authority.",
|
||||
"about.proxy.loopback": "Bind llmgw to loopback when the proxy is on the same machine: <code>listen: \"127.0.0.1:8080\"</code>.",
|
||||
@@ -93,7 +88,6 @@
|
||||
"about.proxy.headers.text": "llmgw marks session cookies Secure from <code>X-Forwarded-Proto</code> and shows <code>X-Forwarded-Host</code> as the base URL.",
|
||||
"about.proxy.body.title": "Body limit matches the router",
|
||||
"about.proxy.body.text": "32 MiB, so an oversized request is refused before it is uploaded.",
|
||||
|
||||
"about.download.title": "Download",
|
||||
"about.download.intro": "Built by CI from the tag and checked against <code>checksums.txt</code>. Every release is on the <a href=\"https://gitea.mynodes.xyz/ilker/llmgw/releases\">releases page</a> with its changelog.",
|
||||
"about.download.released": "Released %s",
|
||||
@@ -109,7 +103,6 @@
|
||||
"about.download.checksums": "Checksums",
|
||||
"about.download.checksums_note": "SHA-256 of every file above.",
|
||||
"about.download.dev": "This is a development build (%s). Take a release from the <a href=\"https://gitea.mynodes.xyz/ilker/llmgw/releases\">releases page</a>, where every file is listed with its checksum.",
|
||||
|
||||
"about.status.title": "Where it stands",
|
||||
"about.status.intro": "Pre-1.0: configuration formats may change between minor versions. This is what has run on real hardware, and what has not yet.",
|
||||
"about.status.proven": "Run on real clusters",
|
||||
@@ -122,8 +115,21 @@
|
||||
"about.status.open": "Not yet proven",
|
||||
"about.status.open.fabrics": "InfiniBand and NVLink on a real site: written and tested against the real output of <code>sminfo</code>, <code>ibnetdiscover</code> and <code>nvidia-smi</code>, but not yet run on the hardware. Run <code>llmgw doctor --json</code> on a few nodes first.",
|
||||
"about.status.open.planes": "Two control planes over long periods",
|
||||
"about.status.open.weights": "Nodes do not share downloaded weights; the mirror keeps each download local",
|
||||
|
||||
"about.status.open.weights": "The model mirror on one machine: the last single point in a high-availability install",
|
||||
"about.footer.changelog": "Changelog",
|
||||
"about.footer.reference": "Configuration reference"
|
||||
"about.footer.reference": "Configuration reference",
|
||||
"about.what.knowledge.title": "Answers from your documents",
|
||||
"about.what.knowledge.text": "People add PDF, Office and mail from their own machines with <code>llmgw-kb</code>; only text and vectors are sent. Each organisation, team and project sees only its own collections, down to the database row.",
|
||||
"about.what.guard.title": "Guardrails on every request",
|
||||
"about.what.guard.text": "Personal data, secrets, prompt injection and deny patterns, with a judge model on CPUs. Policies observe or enforce, per organisation, team or project; an answer nobody could check is refused.",
|
||||
"about.what.signin.title": "Sign-in your company already has",
|
||||
"about.what.signin.text": "Entra ID, Active Directory or any OIDC provider; users and groups by SCIM; staff roles, and an audit log of every change.",
|
||||
"about.what.ha.title": "High availability from the first install",
|
||||
"about.what.ha.text": "Three machines: each can run the gateway, PostgreSQL under Patroni keeps the store on two, etcd votes on all three. Setup builds it; losing any one machine loses nothing.",
|
||||
"about.what.media.title": "Pictures and 3D models too",
|
||||
"about.what.media.text": "ComfyUI on the same cards: pictures and meshes as jobs, queued, followed and returned as files, each carrying the licence it was made under.",
|
||||
"about.install.cta": "Every step, with its screens",
|
||||
"about.status.proven.ha": "Three-machine high availability built by the setup screens; a lost store primary cost no requests",
|
||||
"about.status.proven.signin": "Sign-in with Active Directory, tried against a Samba domain controller, and with OIDC",
|
||||
"about.status.proven.kb": "Knowledge bases of over 100,000 passages, answered from with their sources"
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
{
|
||||
"install.meta.description": "Installing llmgw, step by step: what each machine needs, then every screen of a real three-machine install, recorded.",
|
||||
"install.title": "Install guide",
|
||||
"install.nav.label": "Install guide",
|
||||
"install.nav.before": "Before you start",
|
||||
"install.nav.binary": "Get llmgw",
|
||||
"install.nav.setup": "Set up",
|
||||
"install.nav.check": "Check it",
|
||||
"install.nav.use": "First request",
|
||||
"install.nav.next": "What next",
|
||||
"install.nav.trouble": "If it goes wrong",
|
||||
"install.nav.about": "About llmgw",
|
||||
"install.step": "Step %s",
|
||||
"install.hero.eyebrow": "Install guide · recorded with %s",
|
||||
"install.hero.title": "From three bare machines to a <em>first answer</em>.",
|
||||
"install.hero.lede": "Every screen on this page is from a real install: three fresh Ubuntu 24.04 machines and the release anyone can download. The only change made beforehand was turning SSH password login on, as a stock Ubuntu server has it. Read the step, see the screen, or press play and watch the session as it ran.",
|
||||
"install.hero.machines": "machines, high availability",
|
||||
"install.hero.steps": "steps, after the checks",
|
||||
"install.hero.time": "of installing, once the questions are answered",
|
||||
"install.hero.hidden": "screens left out",
|
||||
"install.before.title": "Before you start",
|
||||
"install.before.intro": "One machine, or three for high availability. Check each one first; the setup screens check them again before they install anything.",
|
||||
"install.before.commands": "The same checks by hand, on each machine:",
|
||||
"install.req.machines": "Machines",
|
||||
"install.req.machines.text": "One, or three for high availability: each of the three can run the gateway, two keep the store replicated, and losing any one loses nothing.",
|
||||
"install.req.system": "System",
|
||||
"install.req.system.text": "Ubuntu 22.04 or 24.04, Debian 12 or RHEL 10; x86-64 or ARM64.",
|
||||
"install.req.size": "Size",
|
||||
"install.req.size.text": "At least 2 CPUs and 3.5 GB of memory on each; 15 GB of free disk on the first machine and 30 GB on the two that keep the store.",
|
||||
"install.req.clock": "Clock",
|
||||
"install.req.clock.text": "Synchronised: the machines must agree within a second.",
|
||||
"install.req.network": "Network",
|
||||
"install.req.network.text": "The machines reach one another. Users reach the gateway on <code>8080</code>; nodes reach <code>7443</code> and <code>8081</code>. Internet access to download engines and models, or an offline bundle.",
|
||||
"install.req.access": "Access",
|
||||
"install.req.access.text": "<code>sudo</code> on every machine. SSH from the first machine to the other two, with a password, a key or the ssh agent; a machine nothing may log into joins with a code instead.",
|
||||
"install.req.names": "Names",
|
||||
"install.req.names.text": "A hostname of its own on each machine. It becomes the machine's name in the cluster.",
|
||||
"install.req.gpus": "GPU nodes",
|
||||
"install.req.gpus.text": "Added later, with the NVIDIA driver installed. llmgw installs llama.cpp, vLLM, Python and CUDA itself.",
|
||||
"install.req.memory_note": "<strong>Memory, when the first machine also serves models.</strong> In this recording it runs the starter model and both guardrail judges. With 8 GB there was no room left for the starter model; with 12 GB everything fits.",
|
||||
"install.binary.title": "Get llmgw",
|
||||
"install.binary.intro": "One static file, the same on every machine. Only the first needs it: setup copies it to the others.",
|
||||
"install.binary.text": "The latest release, its checksum checked. <code>sha256sum</code> must say <code>OK</code>; if it does not, the download is damaged.",
|
||||
"install.setup.title": "Set up",
|
||||
"install.setup.intro": "With nothing installed, llmgw opens its setup screens. They ask, check every machine, show the plan, and install.",
|
||||
"install.setup.text": "On the first machine:",
|
||||
"install.setup.keys": "Keyboard only: arrows to move, Space to turn a part on or off, Enter to open its settings or go on, Esc to go back. It works over a serial console too.",
|
||||
"install.sc1.tab": "One or three",
|
||||
"install.sc1.text": "<strong>One machine or three.</strong> Nothing of llmgw is installed yet, so the setup starts here. One machine runs everything; three are high availability.",
|
||||
"install.sc2.tab": "What to install",
|
||||
"install.sc2.text": "<strong>Every part, on or off.</strong> Enter on a part opens its settings and says what it does. Here this machine is also a node, with the small starter model, so there is something to ask the moment the install ends.",
|
||||
"install.sc3.tab": "Administrator",
|
||||
"install.sc3.text": "<strong>The control plane's settings.</strong> The administrator's email (a password is made for it and shown once, at the end), other names nodes reach this machine by, the portal's language, and whether people may sign up.",
|
||||
"install.sc4.tab": "The other two",
|
||||
"install.sc4.text": "<strong>The machines that keep the store.</strong> Address, user, and how to log in: the ssh agent, a key file, a password or a join code. Passwords are kept in memory only, for this setup.",
|
||||
"install.sc5.tab": "Checks",
|
||||
"install.sc5.text": "<strong>Every machine, checked before anything is installed:</strong> login, system, processor, CPUs, memory, disk, clock, ports, internet and whether the machines reach one another. Fix what fails and press R.",
|
||||
"install.sc6.tab": "Plan",
|
||||
"install.sc6.text": "<strong>What will be installed, and what is left out.</strong> Esc goes back to change it; Enter installs.",
|
||||
"install.sc7.tab": "Install",
|
||||
"install.sc7.text": "<strong>Installed while you watch:</strong> the certificate authority and certificates, etcd on all three, PostgreSQL under Patroni on the other two, the control plane on all three, the mirror, the guardrails' judges and the starter model.",
|
||||
"install.sc8.tab": "Done",
|
||||
"install.sc8.text": "<strong>The administrator's password, shown once.</strong> Copy it now. Below it, the same install as one command, for a script.",
|
||||
"install.setup.whole": "The whole setup, as it ran",
|
||||
"install.setup.script.title": "The same, from a script",
|
||||
"install.setup.script.intro": "What the last screen prints. Safe to run again: it keeps what exists and finishes what is missing.",
|
||||
"install.check.title": "Check it",
|
||||
"install.check.intro": "From any of the three machines.",
|
||||
"install.check.text": "<code>llmgw status</code> is the whole system in a few lines: nodes, replicas, models, which control plane leads, and the store. Run again, <code>sudo llmgw</code> is the console. In a browser, <code>http://<first machine>:8080</code> is the portal; sign in with the password from the last setup screen.",
|
||||
"install.use.title": "The first request",
|
||||
"install.use.intro": "With a key from the account page.",
|
||||
"install.use.text": "On the account page, <strong>Create a key</strong>: it is shown once. Any OpenAI client works with it; only the base URL changes, to <code>http://<first machine>:8080/v1</code>. The chat page answers too, with the same models.",
|
||||
"install.cast.0-prereq": "The checks on the first machine, then a name for each of the three.",
|
||||
"install.cast.1-binary": "The latest release, its checksum checked, installed.",
|
||||
"install.cast.2-setup": "Every screen of the setup, from the first question to the administrator's password. Long waits are shortened; nothing is left out.",
|
||||
"install.cast.3-check": "The system's state, then the console.",
|
||||
"install.cast.4-request": "A key from the account page, and the starter model answering.",
|
||||
"install.shot.login": "The portal's sign-in, at port 8080 of the first machine.",
|
||||
"install.shot.admin": "The cluster page: the node, its memory and disk, and the models with what each holds.",
|
||||
"install.shot.newkey": "A key made on the account page, shown once, with a request to copy.",
|
||||
"install.shot.chat": "The chat page, with the starter model.",
|
||||
"install.next.title": "What next",
|
||||
"install.next.intro": "Each is in the console, under Do, as well as on the command line.",
|
||||
"install.next.nodes.title": "GPU nodes",
|
||||
"install.next.nodes.text": "<code>sudo llmgw node add <user>@<address></code> logs in, installs llmgw, checks the node with <code>llmgw doctor</code>, and starts its agent. The node installs its engines itself.",
|
||||
"install.next.models.title": "Models",
|
||||
"install.next.models.text": "<code>/admin/mirror</code> → Explore models: what your nodes can run, downloaded once into the mirror, then served always on or on demand.",
|
||||
"install.next.people.title": "People",
|
||||
"install.next.people.text": "Accounts, organisations and projects, budgets and limits; single sign-on with Entra ID, Active Directory or OIDC.",
|
||||
"install.next.https.title": "HTTPS",
|
||||
"install.next.https.text": "Put Caddy or nginx in front of port 8080 before anyone outside a trusted network uses it. The configurations are on the <a href=\"%s#proxy\">about page</a>.",
|
||||
"install.trouble.title": "If it goes wrong",
|
||||
"install.trouble.intro": "What this install met, and what else is common.",
|
||||
"install.trouble.see": "What you see",
|
||||
"install.trouble.do": "What to do",
|
||||
"install.trouble.starter.see": "<code>starter model … has nowhere to run</code> at the end of the install",
|
||||
"install.trouble.starter.do": "The first machine has no room left after the judges. <code>llmgw ctl explain-placement <model></code> says what it lacks. Give it more memory, or add a node, then run <code>sudo llmgw</code> again: it finishes what is missing.",
|
||||
"install.trouble.judge.see": "An answer comes back empty, with <code>\"finish_reason\": \"content_filter\"</code>",
|
||||
"install.trouble.judge.do": "The guardrails' judge gave no verdict in time, and an unchecked answer is refused, not let through. The judge needs free cores; on a machine shared with a model it can miss its budget. Give the judges more cores, or a machine of their own.",
|
||||
"install.trouble.ssh.see": "The checks cannot log in to another machine",
|
||||
"install.trouble.ssh.do": "Many cloud images turn SSH password login off. Use a key file or the ssh agent instead, or a join code: <code>llmgw join <code></code> on that machine.",
|
||||
"install.trouble.check.see": "A check fails: ports, clock, disk",
|
||||
"install.trouble.check.do": "Each says what it found. Fix it on that machine and press R on the checks screen.",
|
||||
"install.trouble.node.see": "Anything else about a machine",
|
||||
"install.trouble.node.do": "<code>sudo llmgw doctor</code> on it. <code>--json</code> writes everything it read, to send to whoever helps you.",
|
||||
"install.footer.recorded": "screens recorded with %s",
|
||||
"install.footer.full": "The full install guide",
|
||||
"install.play": "Play",
|
||||
"install.pause": "Pause",
|
||||
"install.again": "Play again",
|
||||
"install.tty_label": "A terminal screen, recorded",
|
||||
"install.hero.time_value": "~3 min"
|
||||
}
|
||||
@@ -9,7 +9,6 @@
|
||||
"about.nav.download": "İndir",
|
||||
"about.nav.source": "Kaynak kod",
|
||||
"about.nav.gateway": "Bu ağ geçidi",
|
||||
|
||||
"about.hero.eyebrow": "Kendi sunucunuzda çalışan LLM ağ geçidi · %s",
|
||||
"about.hero.title": "<em>Kendi GPU'larınızın</em> önünde tek bir uç nokta.",
|
||||
"about.hero.lede": "llmgw; vLLM, SGLang ve llama.cpp'yi sunucularınızda çalıştırır ve hepsini OpenAI uyumlu tek bir API'nin arkasından sunar. Hangi GPU'ların aynı NVLink alanını ya da aynı InfiniBand fabric'ini paylaştığını bilir; makinelere bölünmüş bir model hiçbir zaman iki fabric'e birden yayılmaz.",
|
||||
@@ -22,7 +21,6 @@
|
||||
"about.schem.title": "İstemciler tek bir llmgw adresine ulaşır. llmgw bir modelin parçalarını her seferinde tek bir fabric içindeki GPU'lara yerleştirir.",
|
||||
"about.schem.editor": "editör ajanı",
|
||||
"about.schem.caption": "Birden çok düğüme yayılan bir replika, yeri varsa tek bir NVLink alanına, yoksa tek bir InfiniBand fabric'ine yerleşir. Birbirini göremeyen iki fabric arasında asla köprü kurulmaz: all-reduce takılıp kalırdı.",
|
||||
|
||||
"about.what.title": "Ne yapar",
|
||||
"about.what.intro": "Tek bir statik ikili dosya. Kontrol düzlemi olarak, her GPU düğümünde ajan olarak ve ağırlık aynası olarak çalışır.",
|
||||
"about.what.api.title": "OpenAI API'si, sizin donanımınız",
|
||||
@@ -34,7 +32,7 @@
|
||||
"about.what.keys.title": "Anahtarlar, bütçeler, hız sınırları",
|
||||
"about.what.keys.text": "Kuruluşlar, ekipler ve projeler; her birinin kendi sınırları var. Küme dolduğunda kimin bekleyeceğine öncelik bantları karar verir. Yöneticiler kuruluşlarının harcamasını, kişiler kendi harcamalarını görür.",
|
||||
"about.what.editors.title": "Editörler hesap sayfasından bağlanır",
|
||||
"about.what.editors.text": "Continue, Codex ya da Cline'ı ve bir model seçin. Sayfa bir anahtar oluşturur ve yapılandırmayı doldurulmuş olarak ya da onu yazan tek satırlık bir komut olarak verir.",
|
||||
"about.what.editors.text": "Hesap sayfasından Continue, Codex ya da Cline: bir anahtar ve doldurulmuş yapılandırma ya da onu yazan tek satırlık bir komut. Claude Code ve OpenCode aynı anahtarla, Anthropic ve OpenAI API'leri üzerinden çalışır.",
|
||||
"about.what.chat.title": "Hızlı sorular için bir sohbet sayfası",
|
||||
"about.what.chat.text": "Markdown ve matematik biçimlenmiş gösterilir, yanıt yarıda durdurulabilir; saniyede token ve ilk token süresi görünür, bir sistem istemi ve sıcaklık ayarlanabilir. Ağ geçidi hiçbir sohbeti saklamaz.",
|
||||
"about.what.mirror.title": "Göz atabileceğiniz bir ayna",
|
||||
@@ -45,7 +43,6 @@
|
||||
"about.what.upgrades.text": "Önce kontrol düzlemini, sonra ajanları yükseltin. Yeniden başlatılan bir ajan, çalıştırdığı motorları yeniden yüklemek yerine devralır; böylece replikalar baştan sona ayakta kalır.",
|
||||
"about.what.dashboard.title": "Hiçbir kurulum gerektirmeyen bir pano",
|
||||
"about.what.dashboard.text": "<code>/admin</code>; GPU sınıfı başına kapasiteyi, düğüm başına CPU ve belleği, kimin neyi kullandığını, her düğümün sayfasını ve günlüklerini gösterir. Telefona sığar. Prometheus isteğe bağlıdır.",
|
||||
|
||||
"about.fabrics.title": "InfiniBand ve NVLink, her birinden birkaç tane",
|
||||
"about.fabrics.intro": "Bir sahada bağımsız InfiniBand fabric'leri, sekiz GPU'lu HGX kartları ve NVL72 kabinleri yan yana bulunabilir. llmgw bunları bir yapılandırma dosyasındaki adlarına göre değil, adaptörlerin bildirdiklerine göre ayırt eder.",
|
||||
"about.fabrics.guide": "Fabric ve NVLink kılavuzu",
|
||||
@@ -66,20 +63,18 @@
|
||||
"about.fabrics.racks.text": "<code>/admin/nvlink</code> hangi tepsilerin katıldığını, hangisinin IMEX'inin çöktüğünü, hangi bağlantının düşük hızda çalıştığını gösterir.",
|
||||
"about.fabrics.ports.title": "Portlar her yaşam sinyalinde yeniden okunur",
|
||||
"about.fabrics.ports.text": "Kapanan bir port, saniyeler içinde fabric'ini düğümden düşürür. Sayaçlar ve hızlar Prometheus'a aktarılır.",
|
||||
|
||||
"about.install.title": "Sıfırdan ilk isteğe",
|
||||
"about.install.intro": "Debian, Ubuntu ya da RHEL 10. NVIDIA sürücülerini ve InfiniBand yığınını siz kurarsınız; gerisini llmgw yapar.",
|
||||
"about.install.guide": "<a href=\"https://gitea.mynodes.xyz/ilker/llmgw/src/branch/main/docs/install.md\">Ayrıntılı kılavuz</a> aynayı, GPU'ları ve bir şey çalışmadığında ne yapılacağını anlatır.",
|
||||
"about.install.intro": "Yüksek erişilebilirlik için üç makine ya da tek makine. Ubuntu, Debian ya da RHEL 10; motorları, Python'u ve CUDA'yı llmgw kendisi kurar.",
|
||||
"about.install.guide": "<a href=\"%s\">Kurulum rehberi</a> her adımı gerçek bir kurulumun kaydedilmiş ekranlarıyla gösterir.",
|
||||
"about.install.binary.title": "İkili dosyayı her makineye koyun",
|
||||
"about.install.binary.text": "Statik, bağımlılıksız; her yerde aynı dosya.",
|
||||
"about.install.server.title": "Kontrol düzlemini kurun",
|
||||
"about.install.server.text": "Kullanıcıyı, sertifika yetkilisini, sertifikayı, <code>server.yaml</code> dosyasını ve yönetici hesabınızı oluşturur, ardından hizmeti başlatır. Yeniden çalıştırmak güvenlidir. Düğümlerin kullandığı ama makinenin kendi adı olarak göremediği her ad için <code>--host</code> ekleyin.",
|
||||
"about.install.node.title": "Bir düğüm ekleyin",
|
||||
"about.install.node.text": "Sertifika yetkilisini barındıran kontrol düzleminden. Düğüm sertifikasını, ikili dosyayı ve ajanı alır, motorunu da kendisi kurar.",
|
||||
"about.install.server.title": "Kurulum ekranlarını çalıştırın",
|
||||
"about.install.server.text": "Hiçbir şey kurulu değilken <code>sudo llmgw</code> kurulumu açar: tek makine ya da üç, ne kurulacağı, diğer makinelerin girişleri. Önce her makineyi denetler, planı gösterir ve kurar. Bir betikten <code>llmgw server init</code> aynısını yapar.",
|
||||
"about.install.node.title": "GPU düğümleri ekleyin",
|
||||
"about.install.node.text": "Kontrol düzleminden, SSH ile. Düğüm llmgw'yi alır, <code>llmgw doctor</code> ile denetlenir, sertifikasını alır ve ajanını başlatır; motorlarını kendisi kurar.",
|
||||
"about.install.node.output_label": "llmgw ctl nodes çıktısından bir örnek",
|
||||
"about.install.model.title": "Bir model ekleyin ve istek gönderin",
|
||||
"about.install.model.text": "Yeniden başlatma gerekmez. Model yerleşmezse <code>llmgw ctl explain-placement</code> her düğümü ve neden geri çevrildiğini tek tek söyler.",
|
||||
|
||||
"about.install.model.text": "<code>/admin/mirror</code> sayfasından düğümlerinizin çalıştırabildikleri, küme için bir kez indirilir; ya da adıyla. Model yerleşmezse nedenini <code>llmgw ctl explain-placement</code> söyler.",
|
||||
"about.proxy.title": "Ters proxy arkasında",
|
||||
"about.proxy.intro": "8080 portu düz HTTP'dir. Onu genel geçer bir sertifikayla Caddy ya da nginx arkasına koyun. Proxy'den yalnızca 8080 geçer: ajanlar 7443'e doğrudan ulaşır, çünkü orası kümenin kendi sertifika yetkilisiyle karşılıklı TLS'dir.",
|
||||
"about.proxy.loopback": "Proxy aynı makinedeyse llmgw'yi loopback adresine bağlayın: <code>listen: \"127.0.0.1:8080\"</code>.",
|
||||
@@ -93,7 +88,6 @@
|
||||
"about.proxy.headers.text": "llmgw oturum çerezlerini <code>X-Forwarded-Proto</code> başlığına göre Secure olarak işaretler ve <code>X-Forwarded-Host</code> değerini temel URL olarak gösterir.",
|
||||
"about.proxy.body.title": "Gövde sınırı yönlendiricininkiyle aynıdır",
|
||||
"about.proxy.body.text": "32 MiB; böylece fazla büyük bir istek daha yüklenmeden reddedilir.",
|
||||
|
||||
"about.download.title": "İndir",
|
||||
"about.download.intro": "CI tarafından etiketten derlenir ve <code>checksums.txt</code> ile doğrulanır. Her sürüm, değişiklik günlüğüyle birlikte <a href=\"https://gitea.mynodes.xyz/ilker/llmgw/releases\">sürümler sayfasında</a> yer alır.",
|
||||
"about.download.released": "Yayın tarihi: %s",
|
||||
@@ -109,7 +103,6 @@
|
||||
"about.download.checksums": "Sağlama toplamları",
|
||||
"about.download.checksums_note": "Yukarıdaki her dosyanın SHA-256 özeti.",
|
||||
"about.download.dev": "Bu bir geliştirme derlemesidir (%s). Her dosyanın sağlama toplamıyla listelendiği <a href=\"https://gitea.mynodes.xyz/ilker/llmgw/releases\">sürümler sayfasından</a> bir sürüm edinin.",
|
||||
|
||||
"about.status.title": "Proje ne durumda",
|
||||
"about.status.intro": "1.0 öncesi: yapılandırma biçimleri ara sürümler arasında değişebilir. Aşağıda gerçek donanımda çalışmış olanlar ve henüz çalışmamış olanlar var.",
|
||||
"about.status.proven": "Gerçek kümelerde çalıştı",
|
||||
@@ -122,8 +115,21 @@
|
||||
"about.status.open": "Henüz kanıtlanmadı",
|
||||
"about.status.open.fabrics": "Gerçek bir sahada InfiniBand ve NVLink: <code>sminfo</code>, <code>ibnetdiscover</code> ve <code>nvidia-smi</code> komutlarının gerçek çıktısına göre yazıldı ve sınandı, ama henüz o donanımda çalıştırılmadı. Önce birkaç düğümde <code>llmgw doctor --json</code> çalıştırın.",
|
||||
"about.status.open.planes": "Uzun süre boyunca iki kontrol düzlemi",
|
||||
"about.status.open.weights": "Düğümler indirilen ağırlıkları paylaşmaz; ayna her indirmeyi yerel tutar",
|
||||
|
||||
"about.status.open.weights": "Model aynası tek makinede: yüksek erişilebilir bir kurulumdaki son tek nokta",
|
||||
"about.footer.changelog": "Değişiklik günlüğü",
|
||||
"about.footer.reference": "Yapılandırma başvurusu"
|
||||
"about.footer.reference": "Yapılandırma başvurusu",
|
||||
"about.what.knowledge.title": "Belgelerinizden yanıtlar",
|
||||
"about.what.knowledge.text": "Kişiler PDF, Office ve e-postaları kendi makinelerinden <code>llmgw-kb</code> ile ekler; yalnızca metin ve vektörler gönderilir. Her kuruluş, ekip ve proje veritabanı satırına kadar yalnızca kendi koleksiyonlarını görür.",
|
||||
"about.what.guard.title": "Her istekte koruma",
|
||||
"about.what.guard.text": "Kişisel veriler, gizli bilgiler, istem enjeksiyonu ve yasaklı kalıplar; işlemcilerde çalışan bir hakem modeliyle. Politikalar kuruluş, ekip ya da proje başına izler ya da uygular; kimsenin denetleyemediği bir yanıt reddedilir.",
|
||||
"about.what.signin.title": "Şirketinizin zaten kullandığı oturum açma",
|
||||
"about.what.signin.text": "Entra ID, Active Directory ya da herhangi bir OIDC sağlayıcısı; kullanıcılar ve gruplar SCIM ile; yönetici rolleri ve her değişikliğin denetim kaydı.",
|
||||
"about.what.ha.title": "İlk kurulumdan yüksek erişilebilirlik",
|
||||
"about.what.ha.text": "Üç makine: her biri ağ geçidini çalıştırabilir, veritabanını ikisinde Patroni altında PostgreSQL tutar, etcd üçünde de oy verir. Kurulum bunu kendisi kurar; herhangi bir makinenin kaybı hiçbir şey kaybettirmez.",
|
||||
"about.what.media.title": "Görseller ve 3D modeller de",
|
||||
"about.what.media.text": "Aynı kartlarda ComfyUI: görseller ve 3D modeller iş olarak kuyruğa alınır, izlenir ve dosya olarak döner; her biri üretildiği lisansı taşır.",
|
||||
"about.install.cta": "Her adım, ekranlarıyla",
|
||||
"about.status.proven.ha": "Kurulum ekranlarının kurduğu üç makineli yüksek erişilebilirlik; veritabanının birincil makinesinin kaybı hiçbir isteğe mal olmadı",
|
||||
"about.status.proven.signin": "Bir Samba etki alanı denetleyicisine karşı denenmiş Active Directory ile ve OIDC ile oturum açma",
|
||||
"about.status.proven.kb": "100.000'den fazla parçalık, kaynaklarıyla yanıt veren bilgi tabanları"
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
{
|
||||
"install.meta.description": "llmgw kurulumu adım adım: her makinenin neye ihtiyacı olduğu, ardından gerçek bir üç makineli kurulumun her ekranı, kayıt olarak.",
|
||||
"install.title": "Kurulum rehberi",
|
||||
"install.nav.label": "Kurulum rehberi",
|
||||
"install.nav.before": "Başlamadan önce",
|
||||
"install.nav.binary": "llmgw'yi edinin",
|
||||
"install.nav.setup": "Kurun",
|
||||
"install.nav.check": "Kontrol edin",
|
||||
"install.nav.use": "İlk istek",
|
||||
"install.nav.next": "Sonrası",
|
||||
"install.nav.trouble": "Bir şey ters giderse",
|
||||
"install.nav.about": "llmgw hakkında",
|
||||
"install.step": "Adım %s",
|
||||
"install.hero.eyebrow": "Kurulum rehberi · %s ile kaydedildi",
|
||||
"install.hero.title": "Üç boş makineden <em>ilk yanıta</em>.",
|
||||
"install.hero.lede": "Bu sayfadaki her ekran gerçek bir kurulumdan: üç yeni Ubuntu 24.04 makinesi ve herkesin indirebildiği sürüm. Önceden yapılan tek değişiklik, standart bir Ubuntu sunucusundaki gibi SSH'ta parolayla girişi açmaktı. Adımı okuyun, ekranı görün ya da oynatıp oturumu olduğu gibi izleyin.",
|
||||
"install.hero.machines": "makine, yüksek erişilebilirlik",
|
||||
"install.hero.steps": "adım, kontrollerden sonra",
|
||||
"install.hero.time": "kurulum, sorular yanıtlandıktan sonra",
|
||||
"install.hero.hidden": "atlanan ekran",
|
||||
"install.before.title": "Başlamadan önce",
|
||||
"install.before.intro": "Tek makine ya da yüksek erişilebilirlik için üç makine. Önce her birini kontrol edin; kurulum ekranları da bir şey kurmadan önce hepsini yeniden denetler.",
|
||||
"install.before.commands": "Aynı kontroller elle, her makinede:",
|
||||
"install.req.machines": "Makineler",
|
||||
"install.req.machines.text": "Bir, ya da yüksek erişilebilirlik için üç: üçünün her biri ağ geçidini çalıştırabilir, ikisi veritabanını eşlenmiş tutar ve herhangi birinin kaybı hiçbir şey kaybettirmez.",
|
||||
"install.req.system": "Sistem",
|
||||
"install.req.system.text": "Ubuntu 22.04 ya da 24.04, Debian 12 ya da RHEL 10; x86-64 ya da ARM64.",
|
||||
"install.req.size": "Boyut",
|
||||
"install.req.size.text": "Her birinde en az 2 işlemci çekirdeği ve 3,5 GB bellek; ilk makinede 15 GB, veritabanını tutan iki makinede 30 GB boş disk.",
|
||||
"install.req.clock": "Saat",
|
||||
"install.req.clock.text": "Eşzamanlı: makineler arasındaki fark bir saniyeyi geçmemeli.",
|
||||
"install.req.network": "Ağ",
|
||||
"install.req.network.text": "Makineler birbirine ulaşır. Kullanıcılar ağ geçidine <code>8080</code> üzerinden, düğümler <code>7443</code> ve <code>8081</code> üzerinden ulaşır. Motorları ve modelleri indirmek için internet erişimi ya da çevrimdışı bir paket.",
|
||||
"install.req.access": "Erişim",
|
||||
"install.req.access.text": "Her makinede <code>sudo</code>. İlk makineden diğer ikisine parola, anahtar ya da ssh ajanı ile SSH; kimsenin giriş yapamadığı bir makine bunun yerine bir kodla katılır.",
|
||||
"install.req.names": "Adlar",
|
||||
"install.req.names.text": "Her makinenin kendine ait bir adı (hostname). Kümede makinenin adı bu olur.",
|
||||
"install.req.gpus": "GPU düğümleri",
|
||||
"install.req.gpus.text": "Sonradan, NVIDIA sürücüsü kurulu olarak eklenir. llama.cpp, vLLM, Python ve CUDA'yı llmgw kendisi kurar.",
|
||||
"install.req.memory_note": "<strong>İlk makine model de çalıştırıyorsa bellek.</strong> Bu kayıtta ilk makine başlangıç modelini ve iki koruma hakemini birlikte çalıştırıyor. 8 GB ile başlangıç modeline yer kalmadı; 12 GB ile hepsi sığıyor.",
|
||||
"install.binary.title": "llmgw'yi edinin",
|
||||
"install.binary.intro": "Tek bir statik dosya, her makinede aynı. Yalnızca ilk makinede gerekir: kurulum onu diğerlerine kendisi kopyalar.",
|
||||
"install.binary.text": "Son sürüm, sağlama toplamı denetlenerek. <code>sha256sum</code> <code>OK</code> demelidir; demiyorsa indirme bozuktur.",
|
||||
"install.setup.title": "Kurun",
|
||||
"install.setup.intro": "Hiçbir şey kurulu değilken llmgw kurulum ekranlarını açar. Sorar, her makineyi denetler, planı gösterir ve kurar.",
|
||||
"install.setup.text": "İlk makinede:",
|
||||
"install.setup.keys": "Yalnızca klavye: ok tuşlarıyla gezinin, boşluk tuşuyla bir parçayı açıp kapatın, Enter ile ayarlarını açın ya da ilerleyin, Esc ile geri dönün. Seri konsoldan da çalışır.",
|
||||
"install.sc1.tab": "Bir ya da üç",
|
||||
"install.sc1.text": "<strong>Bir makine ya da üç.</strong> Henüz llmgw'nin hiçbir parçası kurulu olmadığı için kurulum buradan başlar. Tek makine her şeyi çalıştırır; üç makine yüksek erişilebilirliktir.",
|
||||
"install.sc2.tab": "Ne kurulacak",
|
||||
"install.sc2.text": "<strong>Her parça, açık ya da kapalı.</strong> Bir parçanın üzerinde Enter, ayarlarını açar ve ne işe yaradığını anlatır. Burada bu makine aynı zamanda küçük başlangıç modeliyle bir düğüm; böylece kurulum biter bitmez soracak bir şey olur.",
|
||||
"install.sc3.tab": "Yönetici",
|
||||
"install.sc3.text": "<strong>Kontrol düzleminin ayarları.</strong> Yöneticinin e-postası (onun için bir parola üretilir ve en sonda bir kez gösterilir), düğümlerin bu makineye ulaştığı diğer adlar, portalın dili ve kişilerin kendi kendine kaydolup olamayacağı.",
|
||||
"install.sc4.tab": "Diğer ikisi",
|
||||
"install.sc4.text": "<strong>Veritabanını tutan makineler.</strong> Adres, kullanıcı ve giriş yolu: ssh ajanı, anahtar dosyası, parola ya da katılım kodu. Parolalar yalnızca bu kurulum için, bellekte tutulur.",
|
||||
"install.sc5.tab": "Kontroller",
|
||||
"install.sc5.text": "<strong>Bir şey kurulmadan önce her makine denetlenir:</strong> giriş, sistem, işlemci mimarisi, çekirdekler, bellek, disk, saat, portlar, internet ve makinelerin birbirine ulaşıp ulaşmadığı. Başarısız olanı düzeltip R'ye basın.",
|
||||
"install.sc6.tab": "Plan",
|
||||
"install.sc6.text": "<strong>Nelerin kurulacağı ve nelerin dışarıda kalacağı.</strong> Değiştirmek için Esc ile geri dönün; Enter kurar.",
|
||||
"install.sc7.tab": "Kurulum",
|
||||
"install.sc7.text": "<strong>Siz izlerken kurulanlar:</strong> sertifika yetkilisi ve sertifikalar, üçünde de etcd, diğer ikisinde Patroni altında PostgreSQL, üçünde de kontrol düzlemi, model aynası, koruma hakemleri ve başlangıç modeli.",
|
||||
"install.sc8.tab": "Bitti",
|
||||
"install.sc8.text": "<strong>Yöneticinin parolası, bir kez gösterilir.</strong> Hemen kopyalayın. Altında aynı kurulum, bir betik için tek komut olarak.",
|
||||
"install.setup.whole": "Kurulumun tamamı, olduğu gibi",
|
||||
"install.setup.script.title": "Aynısı, bir betikten",
|
||||
"install.setup.script.intro": "Son ekranın yazdığı komut. Yeniden çalıştırmak güvenlidir: var olanı korur, eksik olanı tamamlar.",
|
||||
"install.check.title": "Kontrol edin",
|
||||
"install.check.intro": "Üç makinenin herhangi birinden.",
|
||||
"install.check.text": "<code>llmgw status</code> tüm sistemi birkaç satırda gösterir: düğümler, kopyalar, modeller, hangi kontrol düzleminin önde olduğu ve veritabanı. Yeniden çalıştırıldığında <code>sudo llmgw</code> konsoldur. Tarayıcıda <code>http://<ilk makine>:8080</code> portaldır; son kurulum ekranındaki parolayla giriş yapın.",
|
||||
"install.use.title": "İlk istek",
|
||||
"install.use.intro": "Hesap sayfasından alınan bir anahtarla.",
|
||||
"install.use.text": "Hesap sayfasında <strong>Anahtar oluştur</strong>: anahtar bir kez gösterilir. Her OpenAI istemcisi onunla çalışır; yalnızca temel adres <code>http://<ilk makine>:8080/v1</code> olarak değişir. Sohbet sayfası da aynı modellerle yanıt verir.",
|
||||
"install.cast.0-prereq": "İlk makinedeki kontroller, ardından üç makinenin her birine bir ad.",
|
||||
"install.cast.1-binary": "Son sürüm, sağlama toplamı denetlenmiş ve kurulmuş.",
|
||||
"install.cast.2-setup": "Kurulumun ilk sorudan yöneticinin parolasına kadar her ekranı. Uzun beklemeler kısaltıldı; hiçbir şey atlanmadı.",
|
||||
"install.cast.3-check": "Sistemin durumu, ardından konsol.",
|
||||
"install.cast.4-request": "Hesap sayfasından bir anahtar ve yanıt veren başlangıç modeli.",
|
||||
"install.shot.login": "Portalın giriş sayfası, ilk makinenin 8080 portunda.",
|
||||
"install.shot.admin": "Küme sayfası: düğüm, belleği ve diski, ve her birinin ne tuttuğuyla modeller.",
|
||||
"install.shot.newkey": "Hesap sayfasında üretilen, bir kez gösterilen anahtar ve kopyalanacak bir istek.",
|
||||
"install.shot.chat": "Başlangıç modeliyle sohbet sayfası.",
|
||||
"install.next.title": "Sonrası",
|
||||
"install.next.intro": "Hepsi komut satırında olduğu gibi konsolda da, Do altında var.",
|
||||
"install.next.nodes.title": "GPU düğümleri",
|
||||
"install.next.nodes.text": "<code>sudo llmgw node add <kullanıcı>@<adres></code> giriş yapar, llmgw'yi kurar, düğümü <code>llmgw doctor</code> ile denetler ve ajanını başlatır. Düğüm motorlarını kendisi kurar.",
|
||||
"install.next.models.title": "Modeller",
|
||||
"install.next.models.text": "<code>/admin/mirror</code> → Modelleri keşfet: düğümlerinızın çalıştırabildikleri, aynaya bir kez indirilir, sonra sürekli açık ya da istek geldikçe sunulur.",
|
||||
"install.next.people.title": "Kişiler",
|
||||
"install.next.people.text": "Hesaplar, kuruluşlar ve projeler, bütçeler ve sınırlar; Entra ID, Active Directory ya da OIDC ile tek oturum açma.",
|
||||
"install.next.https.title": "HTTPS",
|
||||
"install.next.https.text": "Güvenilir bir ağın dışından biri kullanmadan önce 8080 portunun önüne Caddy ya da nginx koyun. Yapılandırmalar <a href=\"%s#proxy\">tanıtım sayfasında</a>.",
|
||||
"install.trouble.title": "Bir şey ters giderse",
|
||||
"install.trouble.intro": "Bu kurulumun karşılaştıkları ve sık görülen diğerleri.",
|
||||
"install.trouble.see": "Gördüğünüz",
|
||||
"install.trouble.do": "Yapılacak",
|
||||
"install.trouble.starter.see": "Kurulumun sonunda <code>starter model … has nowhere to run</code>",
|
||||
"install.trouble.starter.do": "İlk makinede hakemlerden sonra yer kalmamış. Neyin eksik olduğunu <code>llmgw ctl explain-placement <model></code> söyler. Belleğini artırın ya da bir düğüm ekleyin, sonra <code>sudo llmgw</code> komutunu yeniden çalıştırın: eksik olanı tamamlar.",
|
||||
"install.trouble.judge.see": "Yanıt boş geliyor, <code>\"finish_reason\": \"content_filter\"</code> ile",
|
||||
"install.trouble.judge.do": "Koruma hakemi zamanında karar vermedi; denetlenemeyen bir yanıt geçirilmez, reddedilir. Hakemin boş çekirdeğe ihtiyacı var; bir modelle paylaşılan makinede süresini aşabilir. Hakemlere daha çok çekirdek ya da kendilerine ait bir makine verin.",
|
||||
"install.trouble.ssh.see": "Kontroller diğer bir makineye giriş yapamıyor",
|
||||
"install.trouble.ssh.do": "Birçok bulut imajı SSH'ta parolayla girişi kapatır. Bunun yerine anahtar dosyası ya da ssh ajanı kullanın, ya da o makinede bir katılım koduyla: <code>llmgw join <kod></code>.",
|
||||
"install.trouble.check.see": "Bir kontrol başarısız: portlar, saat, disk",
|
||||
"install.trouble.check.do": "Her biri ne bulduğunu söyler. O makinede düzeltin ve kontroller ekranında R'ye basın.",
|
||||
"install.trouble.node.see": "Bir makineyle ilgili başka her şey",
|
||||
"install.trouble.node.do": "O makinede <code>sudo llmgw doctor</code>. <code>--json</code> okuduğu her şeyi, size yardım edecek kişiye göndermek için yazar.",
|
||||
"install.footer.recorded": "ekranlar %s ile kaydedildi",
|
||||
"install.footer.full": "Ayrıntılı kurulum rehberi",
|
||||
"install.play": "Oynat",
|
||||
"install.pause": "Duraklat",
|
||||
"install.again": "Yeniden oynat",
|
||||
"install.tty_label": "Kaydedilmiş bir terminal ekranı",
|
||||
"install.hero.time_value": "~3 dk"
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 202 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 141 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 77 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 224 KiB |
@@ -3,179 +3,7 @@
|
||||
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta name="description" content="{{t "about.meta.description"}}">
|
||||
{{end}}<title>llmgw</title>
|
||||
{{if .Site}}<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Archivo:wdth,wght@112,600;125,800&family=IBM+Plex+Sans:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500&display=swap">
|
||||
{{end}}<style>
|
||||
{{if not .Site}}@font-face{font-family:"Archivo";font-weight:600 800;font-stretch:112% 125%;font-display:swap;src:url("{{asset "fonts/archivo-latin.woff2"}}") format("woff2");unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD}
|
||||
@font-face{font-family:"Archivo";font-weight:600 800;font-stretch:112% 125%;font-display:swap;src:url("{{asset "fonts/archivo-latin-ext.woff2"}}") format("woff2");unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF}
|
||||
@font-face{font-family:"IBM Plex Sans";font-weight:100 700;font-display:swap;src:url("{{asset "fonts/plex-sans-latin.woff2"}}") format("woff2");unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD}
|
||||
@font-face{font-family:"IBM Plex Sans";font-weight:100 700;font-display:swap;src:url("{{asset "fonts/plex-sans-latin-ext.woff2"}}") format("woff2");unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF}
|
||||
@font-face{font-family:"IBM Plex Mono";font-weight:400;font-display:swap;src:url("{{asset "fonts/plex-mono-400-latin.woff2"}}") format("woff2");unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD}
|
||||
@font-face{font-family:"IBM Plex Mono";font-weight:400;font-display:swap;src:url("{{asset "fonts/plex-mono-400-latin-ext.woff2"}}") format("woff2");unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF}
|
||||
@font-face{font-family:"IBM Plex Mono";font-weight:500;font-display:swap;src:url("{{asset "fonts/plex-mono-500-latin.woff2"}}") format("woff2");unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD}
|
||||
@font-face{font-family:"IBM Plex Mono";font-weight:500;font-display:swap;src:url("{{asset "fonts/plex-mono-500-latin-ext.woff2"}}") format("woff2");unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF}
|
||||
{{end}}:root{
|
||||
--bg:#F3F5F5; --surface:#FFFFFF; --ink:#142023; --muted:#56696C; --line:#D3DCDC;
|
||||
--fiber:#0B7F7D; --fiber-soft:#DDEFEE; --om1:#C4621A; --om1-soft:#F6E6D9;
|
||||
--code-bg:#15201F; --code-ink:#DCEAE8; --code-dim:#7F9A97;
|
||||
--display:"Archivo","Arial Narrow",system-ui,sans-serif;
|
||||
--body:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",sans-serif;
|
||||
--mono:"IBM Plex Mono",ui-monospace,"SFMono-Regular",Menlo,monospace;
|
||||
}
|
||||
@media (prefers-color-scheme:dark){
|
||||
:root:not([data-theme="light"]){
|
||||
--bg:#0E1617; --surface:#152022; --ink:#E2ECEB; --muted:#8FA5A4; --line:#26383A;
|
||||
--fiber:#3FC2BC; --fiber-soft:#133634; --om1:#EE9350; --om1-soft:#3A2717;
|
||||
--code-bg:#0A1011; --code-ink:#D5E5E3; --code-dim:#6E8886;
|
||||
color-scheme:dark;
|
||||
}
|
||||
}
|
||||
:root[data-theme="dark"]{
|
||||
--bg:#0E1617; --surface:#152022; --ink:#E2ECEB; --muted:#8FA5A4; --line:#26383A;
|
||||
--fiber:#3FC2BC; --fiber-soft:#133634; --om1:#EE9350; --om1-soft:#3A2717;
|
||||
--code-bg:#0A1011; --code-ink:#D5E5E3; --code-dim:#6E8886;
|
||||
color-scheme:dark;
|
||||
}
|
||||
*{box-sizing:border-box}
|
||||
body{background:var(--bg);color:var(--ink);font:400 16px/1.6 var(--body);margin:0;padding-inline:20px;padding-block:0}
|
||||
.wrap{max-width:1080px;margin:0 auto}
|
||||
a{color:var(--fiber);text-underline-offset:3px}
|
||||
a:focus-visible,button:focus-visible,input:focus-visible+label{outline:2px solid var(--fiber);outline-offset:3px;border-radius:4px}
|
||||
h1,h2,h3{font-family:var(--display);text-wrap:balance;margin:0}
|
||||
h2{font-size:clamp(1.6rem,3.2vw,2.1rem);font-variation-settings:"wdth" 112;font-weight:600;letter-spacing:-.01em}
|
||||
h3{font-size:1.05rem;font-variation-settings:"wdth" 112;font-weight:600}
|
||||
p{margin:0}
|
||||
code,pre{font-family:var(--mono);font-size:.875rem}
|
||||
:not(pre)>code{background:var(--fiber-soft);padding:.1em .35em;border-radius:4px;font-size:.85em}
|
||||
.eyebrow{font:500 .75rem/1 var(--mono);letter-spacing:.12em;text-transform:uppercase;color:var(--muted)}
|
||||
|
||||
/* top bar */
|
||||
.bar{display:flex;align-items:center;justify-content:space-between;gap:16px;padding-block:22px;border-bottom:1px solid var(--line)}
|
||||
.mark{font:800 1.25rem/1 var(--display);font-variation-settings:"wdth" 125;letter-spacing:-.02em;color:var(--ink);text-decoration:none;display:flex;align-items:center;gap:10px}
|
||||
.mark svg{flex:none}
|
||||
.bar nav{display:flex;gap:20px;flex-wrap:wrap;font-size:.9rem}
|
||||
.bar nav a{color:var(--muted);text-decoration:none}
|
||||
.bar nav a:hover{color:var(--ink)}
|
||||
|
||||
/* hero */
|
||||
.hero{display:grid;grid-template-columns:1.05fr .95fr;gap:48px;align-items:center;padding-block:64px 56px}
|
||||
.hero h1{font-size:clamp(2.5rem,6vw,4.4rem);line-height:.98;font-weight:800;font-variation-settings:"wdth" 125;letter-spacing:-.03em}
|
||||
.hero h1 em{font-style:normal;color:var(--fiber)}
|
||||
.lede{margin-top:22px;font-size:1.12rem;color:var(--muted);max-width:34em}
|
||||
.cta{display:flex;flex-wrap:wrap;gap:12px;margin-top:30px}
|
||||
.btn{display:inline-flex;align-items:center;gap:8px;font:600 .95rem/1 var(--body);padding:13px 18px;border-radius:8px;text-decoration:none;border:1px solid var(--fiber);background:var(--fiber);color:var(--bg)}
|
||||
.btn.ghost{background:transparent;color:var(--ink);border-color:var(--line)}
|
||||
.btn.ghost:hover{border-color:var(--ink)}
|
||||
.oneline{margin-top:26px;display:flex;align-items:center;gap:10px;background:var(--code-bg);color:var(--code-ink);border-radius:8px;padding:12px 12px 12px 16px;max-width:100%}
|
||||
.oneline code{flex:1;min-width:0;overflow-x:auto;white-space:nowrap;background:none;padding:0;font-size:.9rem;color:var(--code-ink)}
|
||||
.oneline .p{color:var(--code-dim);user-select:none}
|
||||
|
||||
/* schematic */
|
||||
figure{margin:0}
|
||||
.schem{background:var(--surface);border:1px solid var(--line);border-radius:12px;padding:18px}
|
||||
.schem svg{display:block;width:100%;height:auto}
|
||||
.schem figcaption{margin-top:12px;font-size:.85rem;color:var(--muted)}
|
||||
.s-label{font:500 11px var(--mono);fill:var(--muted)}
|
||||
.s-strong{font:600 13px var(--body);fill:var(--ink)}
|
||||
.s-box{fill:var(--bg);stroke:var(--line)}
|
||||
.s-gw{fill:var(--fiber-soft);stroke:var(--fiber);stroke-width:1.5}
|
||||
.s-fab{fill:none;stroke:var(--line);stroke-dasharray:5 4}
|
||||
.s-link{fill:none;stroke:var(--line);stroke-width:2}
|
||||
.s-gpu-a{fill:var(--fiber)} .s-gpu-b{fill:var(--om1)}
|
||||
.s-gpu-idle{fill:var(--line)}
|
||||
.pulse{fill:var(--fiber)}
|
||||
.pulse.b{fill:var(--om1)}
|
||||
@media (prefers-reduced-motion:reduce){.pulse{display:none}}
|
||||
|
||||
/* sections */
|
||||
section{padding-block:56px;border-top:1px solid var(--line)}
|
||||
.sec-head{display:grid;grid-template-columns:260px 1fr;gap:40px}
|
||||
.sec-head .intro{color:var(--muted);margin-top:12px;font-size:.95rem}
|
||||
|
||||
.facts{display:grid;grid-template-columns:1fr 1fr;gap:28px 40px}
|
||||
.fact h3{display:flex;gap:10px;align-items:baseline}
|
||||
.fact h3::before{content:"";width:10px;height:10px;border-radius:2px;background:var(--fiber);flex:none;transform:translateY(-1px)}
|
||||
.fact p{margin-top:6px;color:var(--muted);font-size:.95rem;max-width:36em}
|
||||
|
||||
/* steps */
|
||||
.steps{list-style:none;margin:0;padding:0;display:grid;gap:28px;counter-reset:s}
|
||||
.steps li{display:grid;grid-template-columns:44px 1fr;gap:4px 16px;counter-increment:s}
|
||||
.steps li>*{grid-column:2}
|
||||
.steps li::before{content:counter(s);grid-column:1;grid-row:span 4;font:800 1.5rem/1.1 var(--display);font-variation-settings:"wdth" 125;color:var(--fiber)}
|
||||
.steps p{color:var(--muted);font-size:.95rem;max-width:40em}
|
||||
.code{position:relative;margin-top:10px;background:var(--code-bg);color:var(--code-ink);border-radius:8px;min-width:0}
|
||||
.code pre{margin:0;padding:16px 18px;overflow-x:auto;line-height:1.55}
|
||||
.code .c{color:var(--code-dim)}
|
||||
.copy{position:absolute;top:8px;right:8px;font:500 .75rem/1 var(--body);padding:7px 10px;border-radius:6px;border:1px solid #2d4442;background:#1c2b2a;color:var(--code-ink);cursor:pointer}
|
||||
.copy:hover{border-color:var(--code-dim)}
|
||||
|
||||
/* nodes table in step */
|
||||
.term{margin-top:10px;background:var(--code-bg);color:var(--code-ink);border-radius:8px;overflow-x:auto}
|
||||
.term pre{margin:0;padding:14px 18px;font-size:.8rem;line-height:1.6}
|
||||
.term .ok{color:#6FD3A6}
|
||||
|
||||
/* proxy tabs */
|
||||
.tabs{min-width:0}
|
||||
.tabs input{position:absolute;opacity:0;pointer-events:none}
|
||||
.tablist{display:flex;gap:4px;border-bottom:1px solid var(--line)}
|
||||
.tablist label{cursor:pointer;padding:10px 16px;font:600 .9rem/1 var(--body);color:var(--muted);border-bottom:2px solid transparent;margin-bottom:-1px}
|
||||
#t-caddy:checked~.tablist label[for="t-caddy"],#t-nginx:checked~.tablist label[for="t-nginx"]{color:var(--ink);border-color:var(--fiber)}
|
||||
.panel{display:none;margin-top:16px}
|
||||
#t-caddy:checked~.p-caddy,#t-nginx:checked~.p-nginx{display:block}
|
||||
.panel .meta{display:flex;flex-wrap:wrap;justify-content:space-between;gap:8px 16px;font-size:.88rem;color:var(--muted)}
|
||||
.panel .code pre{max-height:460px}
|
||||
.rules{margin-top:28px;display:grid;grid-template-columns:1fr 1fr;gap:18px 32px;padding:0;list-style:none}
|
||||
.rules li{font-size:.92rem;color:var(--muted);padding-left:16px;border-left:2px solid var(--om1)}
|
||||
.rules strong{color:var(--ink);font-weight:600;display:block}
|
||||
|
||||
/* downloads */
|
||||
.dl{width:100%;border-collapse:collapse;font-size:.92rem}
|
||||
.dl-wrap{overflow-x:auto;border:1px solid var(--line);border-radius:10px;background:var(--surface)}
|
||||
.dl th{text-align:left;font:500 .72rem/1 var(--mono);letter-spacing:.1em;text-transform:uppercase;color:var(--muted);padding:14px 16px;border-bottom:1px solid var(--line)}
|
||||
.dl td{padding:12px 16px;border-bottom:1px solid var(--line);vertical-align:top}
|
||||
.dl tr:last-child td{border-bottom:0}
|
||||
.dl td:first-child{white-space:nowrap;font-weight:500}
|
||||
.dl code{background:none;padding:0;font-size:.85rem}
|
||||
.dl .note{color:var(--muted)}
|
||||
.ver{display:flex;flex-wrap:wrap;gap:10px 24px;align-items:baseline;margin-bottom:18px}
|
||||
.tag{font:500 .8rem/1 var(--mono);padding:6px 9px;border-radius:5px;background:var(--fiber-soft);color:var(--fiber)}
|
||||
|
||||
/* status */
|
||||
.status{display:grid;grid-template-columns:1fr 1fr;gap:32px}
|
||||
.status h3{margin-bottom:10px}
|
||||
.status ul{margin:0;padding:0;list-style:none;display:grid;gap:10px}
|
||||
.status li{font-size:.93rem;color:var(--muted);display:grid;grid-template-columns:18px 1fr;gap:8px}
|
||||
.status li::before{font:600 .9rem/1.6 var(--mono)}
|
||||
.proven li::before{content:"✓";color:var(--fiber)}
|
||||
.open li::before{content:"○";color:var(--om1)}
|
||||
|
||||
.next{font:500 .66rem/1 var(--mono);letter-spacing:.08em;text-transform:uppercase;padding:4px 6px;border-radius:4px;background:var(--om1-soft);color:var(--om1);white-space:nowrap;align-self:center}
|
||||
.fab-grid{display:grid;grid-template-columns:1fr;gap:28px;min-width:0}
|
||||
.term .warn{color:#E9B35A} .term .fail{color:#F07C6C} .term .dim{color:var(--code-dim)}
|
||||
.watch{display:grid;grid-template-columns:1fr 1fr;gap:18px 32px;padding:0;margin:0;list-style:none}
|
||||
.watch li{font-size:.92rem;color:var(--muted);padding-left:16px;border-left:2px solid var(--fiber)}
|
||||
.watch strong{color:var(--ink);font-weight:600;display:block}
|
||||
.watch code{font-size:.8em}
|
||||
.order{display:grid;grid-template-columns:repeat(3,1fr);gap:0;border:1px solid var(--line);border-radius:10px;background:var(--surface);overflow:hidden}
|
||||
.order div{padding:16px 18px;border-right:1px solid var(--line)}
|
||||
.order div:last-child{border-right:0}
|
||||
.order b{display:block;font:800 1.4rem/1 var(--display);font-variation-settings:"wdth" 125;color:var(--fiber);margin-bottom:8px}
|
||||
.order b.x{color:var(--om1)}
|
||||
.order h3{font-size:.95rem}
|
||||
.order p{font-size:.88rem;color:var(--muted);margin-top:4px}
|
||||
footer{border-top:1px solid var(--line);padding-block:28px 40px;display:flex;flex-wrap:wrap;justify-content:space-between;gap:12px;font-size:.85rem;color:var(--muted)}
|
||||
footer a{color:var(--muted)}
|
||||
|
||||
@media (max-width:860px){
|
||||
.hero{grid-template-columns:1fr;gap:36px;padding-block:44px 40px}
|
||||
.sec-head{grid-template-columns:1fr;gap:20px}
|
||||
.facts,.rules,.status,.watch,.order{grid-template-columns:1fr}
|
||||
.order div{border-right:0;border-bottom:1px solid var(--line)}
|
||||
.order div:last-child{border-bottom:0}
|
||||
.bar nav{display:none}
|
||||
}
|
||||
</style>
|
||||
{{template "sitestyle" .}}
|
||||
{{if not .Site}}</head><body>{{end}}
|
||||
|
||||
<div class="wrap">
|
||||
@@ -205,7 +33,7 @@ footer a{color:var(--muted)}
|
||||
<p class="lede">{{t "about.hero.lede"}}</p>
|
||||
<div class="cta">
|
||||
<a class="btn" href="#download">{{t "about.hero.download"}}{{with .Release}} {{.}}{{end}}</a>
|
||||
<a class="btn ghost" href="https://gitea.mynodes.xyz/ilker/llmgw/src/branch/main/docs/quick-install.md">{{t "about.hero.guide"}}</a>
|
||||
<a class="btn ghost" href="{{.InstallURL}}">{{t "about.hero.guide"}}</a>
|
||||
</div>
|
||||
<div class="oneline" aria-label="{{t "about.hero.oneline_label"}}">
|
||||
<code><span class="p">$ </span>sudo llmgw server init --admin you@example.com</code>
|
||||
@@ -290,6 +118,16 @@ footer a{color:var(--muted)}
|
||||
<p>{{t "about.what.upgrades.text"}}</p></div>
|
||||
<div class="fact"><h3>{{t "about.what.dashboard.title"}}</h3>
|
||||
<p>{{th "about.what.dashboard.text"}}</p></div>
|
||||
<div class="fact"><h3>{{t "about.what.knowledge.title"}}</h3>
|
||||
<p>{{th "about.what.knowledge.text"}}</p></div>
|
||||
<div class="fact"><h3>{{t "about.what.guard.title"}}</h3>
|
||||
<p>{{t "about.what.guard.text"}}</p></div>
|
||||
<div class="fact"><h3>{{t "about.what.signin.title"}}</h3>
|
||||
<p>{{t "about.what.signin.text"}}</p></div>
|
||||
<div class="fact"><h3>{{t "about.what.ha.title"}}</h3>
|
||||
<p>{{t "about.what.ha.text"}}</p></div>
|
||||
<div class="fact"><h3>{{t "about.what.media.title"}}</h3>
|
||||
<p>{{t "about.what.media.text"}}</p></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -335,7 +173,8 @@ footer a{color:var(--muted)}
|
||||
<div>
|
||||
<h2>{{t "about.install.title"}}</h2>
|
||||
<p class="intro">{{t "about.install.intro"}}</p>
|
||||
<p class="intro">{{th "about.install.guide"}}</p>
|
||||
<p class="intro">{{th "about.install.guide" .InstallURL}}</p>
|
||||
<p class="cta" style="margin-top:18px"><a class="btn" href="{{.InstallURL}}">{{t "about.install.cta"}} →</a></p>
|
||||
</div>
|
||||
<ol class="steps">
|
||||
<li>
|
||||
@@ -350,13 +189,13 @@ sudo install -m0755 llmgw-$VERSION-linux-amd64 /usr/local/bin/llmgw</code></pre>
|
||||
<li>
|
||||
<h3>{{t "about.install.server.title"}}</h3>
|
||||
<p>{{th "about.install.server.text"}}</p>
|
||||
<div class="code"><button class="copy" type="button">{{t "about.copy"}}</button><pre><code>sudo llmgw server init --admin you@example.com</code></pre></div>
|
||||
<div class="code"><button class="copy" type="button">{{t "about.copy"}}</button><pre><code>sudo llmgw</code></pre></div>
|
||||
<a href="{{.InstallURL}}#setup" style="text-decoration:none"><div class="term" aria-label="{{t "install.tty_label"}}"><pre translate="no" style="font-size:clamp(4px,.62vw,.5rem);line-height:1.25">{{.Still "setup-checks"}}</pre></div></a>
|
||||
</li>
|
||||
<li>
|
||||
<h3>{{t "about.install.node.title"}}</h3>
|
||||
<p>{{t "about.install.node.text"}}</p>
|
||||
<div class="code"><button class="copy" type="button">{{t "about.copy"}}</button><pre><code>deploy/bootstrap/onboard-node.sh --node gpu01 --addr 10.0.0.141 \
|
||||
--server https://10.0.0.140:7443 --fabric ib-a:mlx5_0,mlx5_1</code></pre></div>
|
||||
<p>{{th "about.install.node.text"}}</p>
|
||||
<div class="code"><button class="copy" type="button">{{t "about.copy"}}</button><pre><code>sudo llmgw node add ubuntu@10.0.0.21 --fabric ib-a:mlx5_0,mlx5_1</code></pre></div>
|
||||
<div class="term" aria-label="{{t "about.install.node.output_label"}}"><pre>$ llmgw ctl nodes
|
||||
NODE STATE GPUS MEM CLASSES FABRICS REPLICAS LAST SEEN
|
||||
gpu01 <span class="ok">ready</span> 8/8 free 503G h100-80g ib-a 0 2s</pre></div>
|
||||
@@ -364,9 +203,7 @@ gpu01 <span class="ok">ready</span> 8/8 free 503G h100-80g ib-a 0
|
||||
<li>
|
||||
<h3>{{t "about.install.model.title"}}</h3>
|
||||
<p>{{th "about.install.model.text"}}</p>
|
||||
<div class="code"><button class="copy" type="button">{{t "about.copy"}}</button><pre><code>llmgw ctl model add -f qwen-coder-3b.yaml
|
||||
|
||||
curl https://llm.example.com/v1/chat/completions \
|
||||
<div class="code"><button class="copy" type="button">{{t "about.copy"}}</button><pre><code>curl https://llm.example.com/v1/chat/completions \
|
||||
-H "Authorization: Bearer $LLMGW_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"model": "coder", "messages": [{"role": "user", "content": "hello"}]}'</code></pre></div>
|
||||
@@ -456,6 +293,9 @@ curl https://llm.example.com/v1/chat/completions \
|
||||
<li>{{t "about.status.proven.upgrades"}}</li>
|
||||
<li>{{t "about.status.proven.stores"}}</li>
|
||||
<li>{{t "about.status.proven.chat"}}</li>
|
||||
<li>{{t "about.status.proven.ha"}}</li>
|
||||
<li>{{t "about.status.proven.signin"}}</li>
|
||||
<li>{{t "about.status.proven.kb"}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="open">
|
||||
|
||||
@@ -0,0 +1,367 @@
|
||||
{{define "install"}}{{if not .Site}}<!doctype html>
|
||||
<html lang="{{lang}}"><head>
|
||||
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta name="description" content="{{t "install.meta.description"}}">
|
||||
{{end}}<title>{{t "install.title"}} · llmgw</title>
|
||||
{{template "sitestyle" .}}
|
||||
<style>
|
||||
/* the recorded terminal */
|
||||
.tty{margin-top:18px;background:#0f1416;border:1px solid #22302f;border-radius:10px;overflow:hidden;min-width:0}
|
||||
.tty-bar{display:flex;align-items:center;gap:12px;padding:9px 12px;background:#161e20;border-bottom:1px solid #22302f}
|
||||
.tty-dots{display:flex;gap:6px}
|
||||
.tty-dots i{width:11px;height:11px;border-radius:50%;background:#33403f}
|
||||
.tty-title{flex:1;font:500 .75rem/1 var(--mono);color:#7f9a97;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.tty-play{font:600 .78rem/1 var(--body);padding:7px 12px;border-radius:6px;border:1px solid #2d4442;background:#1c2b2a;color:#dceae8;cursor:pointer}
|
||||
.tty-play:hover{border-color:#7f9a97}
|
||||
.tty-screen{margin:0;padding:12px 14px;overflow-x:auto;color:#d6d8d6;font:400 clamp(7px,1.18vw,13px)/1.22 var(--mono);white-space:pre;tab-size:8}
|
||||
.tty-progress{height:3px;background:#1c2b2a}
|
||||
.tty-progress i{display:block;height:3px;width:0;background:var(--fiber);transition:width .2s linear}
|
||||
.tty figcaption,.shot figcaption{padding:10px 14px 12px;font-size:.85rem;color:#9fb3b1;border-top:1px solid #22302f}
|
||||
.shot{margin-top:18px;border:1px solid var(--line);border-radius:10px;overflow:hidden;background:var(--surface)}
|
||||
.shot img{display:block;width:100%;height:auto}
|
||||
.shot figcaption{color:var(--muted);border-top:1px solid var(--line)}
|
||||
.shots{display:grid;grid-template-columns:1fr;gap:18px}
|
||||
.full{margin-top:28px}
|
||||
.req td:first-child{white-space:nowrap}
|
||||
.note-box{margin-top:18px;padding:14px 16px;border-left:3px solid var(--om1);background:var(--om1-soft);border-radius:0 8px 8px 0;font-size:.92rem}
|
||||
.toc{display:flex;flex-wrap:wrap;gap:10px;margin-top:28px;padding:0;list-style:none}
|
||||
.toc a{display:inline-block;padding:8px 12px;border:1px solid var(--line);border-radius:999px;text-decoration:none;color:var(--ink);font-size:.88rem;background:var(--surface)}
|
||||
.toc a b{color:var(--fiber);margin-right:6px}
|
||||
.facts-row{display:flex;flex-wrap:wrap;gap:10px 28px;margin-top:26px;color:var(--muted);font-size:.9rem}
|
||||
.facts-row b{display:block;font:800 1.5rem/1.1 var(--display);font-variation-settings:"wdth" 125;color:var(--ink)}
|
||||
/* the setup screens, one at a time */
|
||||
.screens input{position:absolute;opacity:0;pointer-events:none}
|
||||
.screen-tabs{display:flex;flex-wrap:wrap;gap:6px;margin-top:6px}
|
||||
.screen-tabs label{cursor:pointer;padding:8px 11px;border:1px solid var(--line);border-radius:8px;font:500 .84rem/1 var(--body);color:var(--muted);background:var(--surface)}
|
||||
.screen-tabs label b{color:var(--fiber);margin-right:5px}
|
||||
.screen{display:none}
|
||||
.screen .what{margin-top:16px;max-width:52em;color:var(--muted)}
|
||||
.screen .what strong{color:var(--ink)}
|
||||
#sc1:checked~.screen-tabs label[for="sc1"]{color:var(--ink);border-color:var(--fiber);background:var(--fiber-soft)}
|
||||
#sc1:checked~.sc1{display:block}
|
||||
#sc2:checked~.screen-tabs label[for="sc2"]{color:var(--ink);border-color:var(--fiber);background:var(--fiber-soft)}
|
||||
#sc2:checked~.sc2{display:block}
|
||||
#sc3:checked~.screen-tabs label[for="sc3"]{color:var(--ink);border-color:var(--fiber);background:var(--fiber-soft)}
|
||||
#sc3:checked~.sc3{display:block}
|
||||
#sc4:checked~.screen-tabs label[for="sc4"]{color:var(--ink);border-color:var(--fiber);background:var(--fiber-soft)}
|
||||
#sc4:checked~.sc4{display:block}
|
||||
#sc5:checked~.screen-tabs label[for="sc5"]{color:var(--ink);border-color:var(--fiber);background:var(--fiber-soft)}
|
||||
#sc5:checked~.sc5{display:block}
|
||||
#sc6:checked~.screen-tabs label[for="sc6"]{color:var(--ink);border-color:var(--fiber);background:var(--fiber-soft)}
|
||||
#sc6:checked~.sc6{display:block}
|
||||
#sc7:checked~.screen-tabs label[for="sc7"]{color:var(--ink);border-color:var(--fiber);background:var(--fiber-soft)}
|
||||
#sc7:checked~.sc7{display:block}
|
||||
#sc8:checked~.screen-tabs label[for="sc8"]{color:var(--ink);border-color:var(--fiber);background:var(--fiber-soft)}
|
||||
#sc8:checked~.sc8{display:block}
|
||||
.trouble td{vertical-align:top}
|
||||
.trouble td:first-child{white-space:normal;width:38%}
|
||||
@media (max-width:860px){.shots{grid-template-columns:1fr}}
|
||||
@media (prefers-reduced-motion:reduce){.tty-progress i{transition:none}}
|
||||
</style>
|
||||
{{if not .Site}}</head><body>{{end}}
|
||||
|
||||
<div class="wrap">
|
||||
<header class="bar">
|
||||
<a class="mark" href="{{.AboutURL}}" aria-label="{{t "about.home_label"}}">
|
||||
<svg width="26" height="26" viewBox="0 0 26 26" aria-hidden="true">
|
||||
<rect x="1" y="1" width="24" height="24" rx="6" fill="none" stroke="currentColor" stroke-width="2"/>
|
||||
<path d="M7 9h12M7 13h12M7 17h12" stroke="var(--fiber)" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
llmgw
|
||||
</a>
|
||||
<nav aria-label="{{t "install.nav.label"}}">
|
||||
<a href="#before">{{t "install.nav.before"}}</a>
|
||||
<a href="#binary">{{t "install.nav.binary"}}</a>
|
||||
<a href="#setup">{{t "install.nav.setup"}}</a>
|
||||
<a href="#check">{{t "install.nav.check"}}</a>
|
||||
<a href="#use">{{t "install.nav.use"}}</a>
|
||||
<a href="#trouble">{{t "install.nav.trouble"}}</a>
|
||||
<a href="{{.AboutURL}}">{{t "install.nav.about"}}</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<div class="hero" id="top" style="grid-template-columns:1fr">
|
||||
<div>
|
||||
<p class="eyebrow">{{t "install.hero.eyebrow" .Recorded}}</p>
|
||||
<h1 style="margin-top:14px">{{th "install.hero.title"}}</h1>
|
||||
<p class="lede" style="max-width:46em">{{t "install.hero.lede"}}</p>
|
||||
<div class="facts-row">
|
||||
<span><b>3</b>{{t "install.hero.machines"}}</span>
|
||||
<span><b>4</b>{{t "install.hero.steps"}}</span>
|
||||
<span><b>{{t "install.hero.time_value"}}</b>{{t "install.hero.time"}}</span>
|
||||
<span><b>0</b>{{t "install.hero.hidden"}}</span>
|
||||
</div>
|
||||
<ol class="toc">
|
||||
<li><a href="#before"><b>0</b>{{t "install.nav.before"}}</a></li>
|
||||
<li><a href="#binary"><b>1</b>{{t "install.nav.binary"}}</a></li>
|
||||
<li><a href="#setup"><b>2</b>{{t "install.nav.setup"}}</a></li>
|
||||
<li><a href="#check"><b>3</b>{{t "install.nav.check"}}</a></li>
|
||||
<li><a href="#use"><b>4</b>{{t "install.nav.use"}}</a></li>
|
||||
<li><a href="#next"><b>→</b>{{t "install.nav.next"}}</a></li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section id="before">
|
||||
<div class="sec-head">
|
||||
<div>
|
||||
<p class="eyebrow">{{t "install.step" "0"}}</p>
|
||||
<h2 style="margin-top:8px">{{t "install.before.title"}}</h2>
|
||||
<p class="intro">{{t "install.before.intro"}}</p>
|
||||
</div>
|
||||
<div>
|
||||
<div class="dl-wrap">
|
||||
<table class="dl req">
|
||||
<tbody>
|
||||
<tr><td>{{t "install.req.machines"}}</td><td>{{t "install.req.machines.text"}}</td></tr>
|
||||
<tr><td>{{t "install.req.system"}}</td><td>{{t "install.req.system.text"}}</td></tr>
|
||||
<tr><td>{{t "install.req.size"}}</td><td>{{t "install.req.size.text"}}</td></tr>
|
||||
<tr><td>{{t "install.req.clock"}}</td><td>{{t "install.req.clock.text"}}</td></tr>
|
||||
<tr><td>{{t "install.req.network"}}</td><td>{{th "install.req.network.text"}}</td></tr>
|
||||
<tr><td>{{t "install.req.access"}}</td><td>{{th "install.req.access.text"}}</td></tr>
|
||||
<tr><td>{{t "install.req.names"}}</td><td>{{t "install.req.names.text"}}</td></tr>
|
||||
<tr><td>{{t "install.req.gpus"}}</td><td>{{t "install.req.gpus.text"}}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p class="note-box">{{th "install.req.memory_note"}}</p>
|
||||
<p style="margin-top:18px;color:var(--muted)">{{t "install.before.commands"}}</p>
|
||||
<div class="code"><button class="copy" type="button">{{t "about.copy"}}</button><pre><code>grep PRETTY_NAME /etc/os-release; uname -m; nproc; free -h | grep Mem; df -h /
|
||||
timedatectl | grep synchronized; curl -sI https://gitea.mynodes.xyz | head -1
|
||||
ping -c1 <second machine>; ping -c1 <third machine>
|
||||
sudo -v && echo "sudo works"
|
||||
sudo hostnamectl set-hostname llm-1 <span class="c"># a name of its own, on each machine</span></code></pre></div>
|
||||
</div>
|
||||
</div>
|
||||
{{template "tty" (.Cast "0-prereq")}}
|
||||
</section>
|
||||
|
||||
<section id="binary">
|
||||
<div class="sec-head">
|
||||
<div>
|
||||
<p class="eyebrow">{{t "install.step" "1"}}</p>
|
||||
<h2 style="margin-top:8px">{{t "install.binary.title"}}</h2>
|
||||
<p class="intro">{{t "install.binary.intro"}}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p style="color:var(--muted)">{{th "install.binary.text"}}</p>
|
||||
<div class="code"><button class="copy" type="button">{{t "about.copy"}}</button><pre><code>VERSION=$(curl -fsSL https://gitea.mynodes.xyz/api/v1/repos/ilker/llmgw/releases/latest \
|
||||
| grep -o '"tag_name":"[^"]*"' | cut -d'"' -f4)
|
||||
U=https://gitea.mynodes.xyz/ilker/llmgw/releases/download/$VERSION
|
||||
curl -fsSLO $U/llmgw-$VERSION-linux-amd64 <span class="c"># -linux-arm64 on ARM</span>
|
||||
curl -fsSL $U/checksums.txt | sha256sum -c --ignore-missing
|
||||
sudo install -m0755 llmgw-$VERSION-linux-amd64 /usr/local/bin/llmgw
|
||||
llmgw --version</code></pre></div>
|
||||
</div>
|
||||
</div>
|
||||
{{template "tty" (.Cast "1-binary")}}
|
||||
</section>
|
||||
|
||||
<section id="setup">
|
||||
<div class="sec-head">
|
||||
<div>
|
||||
<p class="eyebrow">{{t "install.step" "2"}}</p>
|
||||
<h2 style="margin-top:8px">{{t "install.setup.title"}}</h2>
|
||||
<p class="intro">{{t "install.setup.intro"}}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p style="color:var(--muted)">{{t "install.setup.text"}}</p>
|
||||
<div class="code"><button class="copy" type="button">{{t "about.copy"}}</button><pre><code>sudo llmgw</code></pre></div>
|
||||
<p style="margin-top:14px;color:var(--muted);font-size:.9rem">{{t "install.setup.keys"}}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="screens full">
|
||||
<input type="radio" name="screen" id="sc1" checked><input type="radio" name="screen" id="sc2"><input type="radio" name="screen" id="sc3"><input type="radio" name="screen" id="sc4">
|
||||
<input type="radio" name="screen" id="sc5"><input type="radio" name="screen" id="sc6"><input type="radio" name="screen" id="sc7"><input type="radio" name="screen" id="sc8">
|
||||
<div class="screen-tabs" role="presentation">
|
||||
<label for="sc1"><b>2.1</b>{{t "install.sc1.tab"}}</label>
|
||||
<label for="sc2"><b>2.2</b>{{t "install.sc2.tab"}}</label>
|
||||
<label for="sc3"><b>2.3</b>{{t "install.sc3.tab"}}</label>
|
||||
<label for="sc4"><b>2.4</b>{{t "install.sc4.tab"}}</label>
|
||||
<label for="sc5"><b>2.5</b>{{t "install.sc5.tab"}}</label>
|
||||
<label for="sc6"><b>2.6</b>{{t "install.sc6.tab"}}</label>
|
||||
<label for="sc7"><b>2.7</b>{{t "install.sc7.tab"}}</label>
|
||||
<label for="sc8"><b>2.8</b>{{t "install.sc8.tab"}}</label>
|
||||
</div>
|
||||
<div class="screen sc1">{{template "still" (.Shot "setup-start" "2.1")}}<p class="what">{{th "install.sc1.text"}}</p></div>
|
||||
<div class="screen sc2">{{template "still" (.Shot "setup-choices" "2.2")}}<p class="what">{{th "install.sc2.text"}}</p></div>
|
||||
<div class="screen sc3">{{template "still" (.Shot "setup-admin" "2.3")}}<p class="what">{{th "install.sc3.text"}}</p></div>
|
||||
<div class="screen sc4">{{template "still" (.Shot "setup-machines" "2.4")}}<p class="what">{{th "install.sc4.text"}}</p></div>
|
||||
<div class="screen sc5">{{template "still" (.Shot "setup-checks" "2.5")}}<p class="what">{{th "install.sc5.text"}}</p></div>
|
||||
<div class="screen sc6">{{template "still" (.Shot "setup-plan" "2.6")}}<p class="what">{{th "install.sc6.text"}}</p></div>
|
||||
<div class="screen sc7">{{template "still" (.Shot "setup-install" "2.7")}}<p class="what">{{th "install.sc7.text"}}</p></div>
|
||||
<div class="screen sc8">{{template "still" (.Shot "setup-done" "2.8")}}<p class="what">{{th "install.sc8.text"}}</p></div>
|
||||
</div>
|
||||
|
||||
<div class="full">
|
||||
<h3>{{t "install.setup.whole"}}</h3>
|
||||
{{template "tty" (.Cast "2-setup")}}
|
||||
</div>
|
||||
|
||||
<div class="full sec-head">
|
||||
<div><h3>{{t "install.setup.script.title"}}</h3><p class="intro">{{t "install.setup.script.intro"}}</p></div>
|
||||
<div class="code"><button class="copy" type="button">{{t "about.copy"}}</button><pre><code>sudo --preserve-env=SSH_AUTH_SOCK llmgw server init \
|
||||
--admin you@example.com \
|
||||
--patroni <user>@<second machine> \
|
||||
--patroni <user>@<third machine> \
|
||||
--standbys
|
||||
|
||||
sudo llmgw server init --admin you@example.com <span class="c"># one machine</span></code></pre></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="check">
|
||||
<div class="sec-head">
|
||||
<div>
|
||||
<p class="eyebrow">{{t "install.step" "3"}}</p>
|
||||
<h2 style="margin-top:8px">{{t "install.check.title"}}</h2>
|
||||
<p class="intro">{{t "install.check.intro"}}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p style="color:var(--muted)">{{th "install.check.text"}}</p>
|
||||
<div class="code"><button class="copy" type="button">{{t "about.copy"}}</button><pre><code>sudo llmgw status
|
||||
sudo llmgw <span class="c"># the console</span></code></pre></div>
|
||||
</div>
|
||||
</div>
|
||||
{{template "tty" (.Cast "3-check")}}
|
||||
<div class="shots">
|
||||
<figure class="shot"><img src="{{.Img "login.png"}}" alt="{{t "install.shot.login"}}" loading="lazy" width="1280" height="650"><figcaption>{{t "install.shot.login"}}</figcaption></figure>
|
||||
<figure class="shot"><img src="{{.Img "admin.png"}}" alt="{{t "install.shot.admin"}}" loading="lazy" width="1280" height="650"><figcaption>{{t "install.shot.admin"}}</figcaption></figure>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="use">
|
||||
<div class="sec-head">
|
||||
<div>
|
||||
<p class="eyebrow">{{t "install.step" "4"}}</p>
|
||||
<h2 style="margin-top:8px">{{t "install.use.title"}}</h2>
|
||||
<p class="intro">{{t "install.use.intro"}}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p style="color:var(--muted)">{{th "install.use.text"}}</p>
|
||||
<div class="code"><button class="copy" type="button">{{t "about.copy"}}</button><pre><code>export LLMGW_API_KEY=sk-llmgw-… <span class="c"># from the account page</span>
|
||||
curl http://<first machine>:8080/v1/chat/completions \
|
||||
-H "Authorization: Bearer $LLMGW_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"model": "qwen-0.5b", "messages": [{"role": "user", "content": "hello"}]}'</code></pre></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="shots">
|
||||
<figure class="shot"><img src="{{.Img "newkey.png"}}" alt="{{t "install.shot.newkey"}}" loading="lazy" width="1280" height="650"><figcaption>{{t "install.shot.newkey"}}</figcaption></figure>
|
||||
<figure class="shot"><img src="{{.Img "chat.png"}}" alt="{{t "install.shot.chat"}}" loading="lazy" width="1280" height="650"><figcaption>{{t "install.shot.chat"}}</figcaption></figure>
|
||||
</div>
|
||||
{{template "tty" (.Cast "4-request")}}
|
||||
</section>
|
||||
|
||||
<section id="next">
|
||||
<div class="sec-head">
|
||||
<div>
|
||||
<h2>{{t "install.next.title"}}</h2>
|
||||
<p class="intro">{{t "install.next.intro"}}</p>
|
||||
</div>
|
||||
<div class="facts">
|
||||
<div class="fact"><h3>{{t "install.next.nodes.title"}}</h3><p>{{th "install.next.nodes.text"}}</p></div>
|
||||
<div class="fact"><h3>{{t "install.next.models.title"}}</h3><p>{{th "install.next.models.text"}}</p></div>
|
||||
<div class="fact"><h3>{{t "install.next.people.title"}}</h3><p>{{th "install.next.people.text"}}</p></div>
|
||||
<div class="fact"><h3>{{t "install.next.https.title"}}</h3><p>{{th "install.next.https.text" .AboutURL}}</p></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="trouble">
|
||||
<div class="sec-head">
|
||||
<div>
|
||||
<h2>{{t "install.trouble.title"}}</h2>
|
||||
<p class="intro">{{t "install.trouble.intro"}}</p>
|
||||
</div>
|
||||
<div class="dl-wrap">
|
||||
<table class="dl trouble">
|
||||
<thead><tr><th>{{t "install.trouble.see"}}</th><th>{{t "install.trouble.do"}}</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>{{th "install.trouble.starter.see"}}</td><td>{{th "install.trouble.starter.do"}}</td></tr>
|
||||
<tr><td>{{th "install.trouble.judge.see"}}</td><td>{{th "install.trouble.judge.do"}}</td></tr>
|
||||
<tr><td>{{th "install.trouble.ssh.see"}}</td><td>{{th "install.trouble.ssh.do"}}</td></tr>
|
||||
<tr><td>{{th "install.trouble.check.see"}}</td><td>{{th "install.trouble.check.do"}}</td></tr>
|
||||
<tr><td>{{th "install.trouble.node.see"}}</td><td>{{th "install.trouble.node.do"}}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<span>llmgw {{.Version}} · {{t "install.footer.recorded" .Recorded}}</span>
|
||||
<span><a href="{{.AboutURL}}">{{t "install.nav.about"}}</a> ·
|
||||
<a href="https://gitea.mynodes.xyz/ilker/llmgw/src/branch/main/docs/install.md">{{t "install.footer.full"}}</a></span>{{if not .Site}}{{template "langs"}}{{end}}
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var say = { copy: {{t "about.copy"}}, copied: {{t "about.copied"}}, selected: {{t "about.selected"}},
|
||||
play: {{t "install.play"}}, pause: {{t "install.pause"}}, again: {{t "install.again"}} };
|
||||
document.querySelectorAll('.copy').forEach(function (b) {
|
||||
b.addEventListener('click', function () {
|
||||
var text = b.parentElement.querySelector('pre').innerText;
|
||||
var done = function (label) { b.textContent = label; setTimeout(function () { b.textContent = say.copy; }, 1600); };
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(text).then(function () { done(say.copied); }, select);
|
||||
} else { select(); }
|
||||
function select() {
|
||||
var r = document.createRange(); r.selectNodeContents(b.parentElement.querySelector('pre'));
|
||||
var s = window.getSelection(); s.removeAllRanges(); s.addRange(r); done(say.selected);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// The player: a recorded session, frame by frame. The page shows its last
|
||||
// screen without it; this only adds the button that plays it back.
|
||||
document.querySelectorAll('.tty[data-src]').forEach(function (el) {
|
||||
var screen = el.querySelector('.tty-screen');
|
||||
var bar = el.querySelector('.tty-progress i'), btn = el.querySelector('.tty-play');
|
||||
var cast = null, total = 0, lines = [], i = 0, elapsed = 0, timer = null;
|
||||
btn.hidden = false;
|
||||
function show(k) {
|
||||
var f = cast.frames[k];
|
||||
if (f.l) { lines = f.l.slice(); } else { for (var r in f.c) { lines[+r] = f.c[r]; } }
|
||||
screen.innerHTML = lines.join('\n');
|
||||
}
|
||||
function tick() {
|
||||
if (i >= cast.frames.length) { timer = null; btn.textContent = say.again; return; }
|
||||
show(i); elapsed += cast.frames[i].d; bar.style.width = (100 * elapsed / total) + '%';
|
||||
timer = setTimeout(tick, cast.frames[i].d * 1000); i++;
|
||||
}
|
||||
function start() {
|
||||
if (i >= cast.frames.length) { i = 0; elapsed = 0; lines = []; }
|
||||
btn.textContent = say.pause; tick();
|
||||
}
|
||||
btn.addEventListener('click', function () {
|
||||
if (timer) { clearTimeout(timer); timer = null; btn.textContent = say.play; return; }
|
||||
if (cast) { start(); return; }
|
||||
// Fetched only now: most readers never play most recordings.
|
||||
btn.disabled = true;
|
||||
fetch(el.dataset.src).then(function (r) { return r.json(); }).then(function (c) {
|
||||
cast = c; total = c.frames.reduce(function (s, f) { return s + f.d; }, 0);
|
||||
// The whole terminal, so the page does not jump as the session fills it.
|
||||
screen.style.minHeight = 'calc(' + (c.rows * 1.22) + 'em + 24px)';
|
||||
btn.disabled = false; start();
|
||||
}, function () { btn.disabled = false; });
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{{if not .Site}}</body></html>{{end}}{{end}}
|
||||
|
||||
{{define "tty"}}<figure class="tty" data-src="{{.Src}}">
|
||||
<div class="tty-bar"><span class="tty-dots" aria-hidden="true"><i></i><i></i><i></i></span>
|
||||
<span class="tty-title" translate="no">ilker@llm-1 · {{.Cols}}×{{.Rows}}</span>
|
||||
<button class="tty-play" type="button" hidden>{{t "install.play"}}</button></div>
|
||||
<pre class="tty-screen" translate="no" aria-label="{{t "install.tty_label"}}">{{.Poster}}</pre>
|
||||
<div class="tty-progress"><i></i></div>
|
||||
<figcaption>{{t (print "install.cast." .Name)}}</figcaption>
|
||||
</figure>{{end}}
|
||||
|
||||
{{define "still"}}<figure class="tty">
|
||||
<div class="tty-bar"><span class="tty-dots" aria-hidden="true"><i></i><i></i><i></i></span>
|
||||
<span class="tty-title" translate="no">sudo llmgw · {{.Label}}</span></div>
|
||||
<pre class="tty-screen" translate="no" aria-label="{{t "install.tty_label"}}">{{.Screen}}</pre>
|
||||
</figure>{{end}}
|
||||
@@ -0,0 +1,176 @@
|
||||
{{/* The look /about and /install share: their fonts, from this binary or,
|
||||
for the published website, from Google; and one stylesheet. */}}
|
||||
{{define "sitestyle"}}{{if .Site}}<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Archivo:wdth,wght@112,600;125,800&family=IBM+Plex+Sans:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500&display=swap">
|
||||
{{end}}<style>
|
||||
{{if not .Site}}@font-face{font-family:"Archivo";font-weight:600 800;font-stretch:112% 125%;font-display:swap;src:url("{{asset "fonts/archivo-latin.woff2"}}") format("woff2");unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD}
|
||||
@font-face{font-family:"Archivo";font-weight:600 800;font-stretch:112% 125%;font-display:swap;src:url("{{asset "fonts/archivo-latin-ext.woff2"}}") format("woff2");unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF}
|
||||
@font-face{font-family:"IBM Plex Sans";font-weight:100 700;font-display:swap;src:url("{{asset "fonts/plex-sans-latin.woff2"}}") format("woff2");unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD}
|
||||
@font-face{font-family:"IBM Plex Sans";font-weight:100 700;font-display:swap;src:url("{{asset "fonts/plex-sans-latin-ext.woff2"}}") format("woff2");unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF}
|
||||
@font-face{font-family:"IBM Plex Mono";font-weight:400;font-display:swap;src:url("{{asset "fonts/plex-mono-400-latin.woff2"}}") format("woff2");unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD}
|
||||
@font-face{font-family:"IBM Plex Mono";font-weight:400;font-display:swap;src:url("{{asset "fonts/plex-mono-400-latin-ext.woff2"}}") format("woff2");unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF}
|
||||
@font-face{font-family:"IBM Plex Mono";font-weight:500;font-display:swap;src:url("{{asset "fonts/plex-mono-500-latin.woff2"}}") format("woff2");unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD}
|
||||
@font-face{font-family:"IBM Plex Mono";font-weight:500;font-display:swap;src:url("{{asset "fonts/plex-mono-500-latin-ext.woff2"}}") format("woff2");unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF}
|
||||
{{end}}:root{
|
||||
--bg:#F3F5F5; --surface:#FFFFFF; --ink:#142023; --muted:#56696C; --line:#D3DCDC;
|
||||
--fiber:#0B7F7D; --fiber-soft:#DDEFEE; --om1:#C4621A; --om1-soft:#F6E6D9;
|
||||
--code-bg:#15201F; --code-ink:#DCEAE8; --code-dim:#7F9A97;
|
||||
--display:"Archivo","Arial Narrow",system-ui,sans-serif;
|
||||
--body:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",sans-serif;
|
||||
--mono:"IBM Plex Mono",ui-monospace,"SFMono-Regular",Menlo,monospace;
|
||||
}
|
||||
@media (prefers-color-scheme:dark){
|
||||
:root:not([data-theme="light"]){
|
||||
--bg:#0E1617; --surface:#152022; --ink:#E2ECEB; --muted:#8FA5A4; --line:#26383A;
|
||||
--fiber:#3FC2BC; --fiber-soft:#133634; --om1:#EE9350; --om1-soft:#3A2717;
|
||||
--code-bg:#0A1011; --code-ink:#D5E5E3; --code-dim:#6E8886;
|
||||
color-scheme:dark;
|
||||
}
|
||||
}
|
||||
:root[data-theme="dark"]{
|
||||
--bg:#0E1617; --surface:#152022; --ink:#E2ECEB; --muted:#8FA5A4; --line:#26383A;
|
||||
--fiber:#3FC2BC; --fiber-soft:#133634; --om1:#EE9350; --om1-soft:#3A2717;
|
||||
--code-bg:#0A1011; --code-ink:#D5E5E3; --code-dim:#6E8886;
|
||||
color-scheme:dark;
|
||||
}
|
||||
*{box-sizing:border-box}
|
||||
body{background:var(--bg);color:var(--ink);font:400 16px/1.6 var(--body);margin:0;padding-inline:20px;padding-block:0}
|
||||
.wrap{max-width:1080px;margin:0 auto}
|
||||
a{color:var(--fiber);text-underline-offset:3px}
|
||||
a:focus-visible,button:focus-visible,input:focus-visible+label{outline:2px solid var(--fiber);outline-offset:3px;border-radius:4px}
|
||||
h1,h2,h3{font-family:var(--display);text-wrap:balance;margin:0}
|
||||
h2{font-size:clamp(1.6rem,3.2vw,2.1rem);font-variation-settings:"wdth" 112;font-weight:600;letter-spacing:-.01em}
|
||||
h3{font-size:1.05rem;font-variation-settings:"wdth" 112;font-weight:600}
|
||||
p{margin:0}
|
||||
code,pre{font-family:var(--mono);font-size:.875rem}
|
||||
:not(pre)>code{background:var(--fiber-soft);padding:.1em .35em;border-radius:4px;font-size:.85em}
|
||||
.eyebrow{font:500 .75rem/1 var(--mono);letter-spacing:.12em;text-transform:uppercase;color:var(--muted)}
|
||||
|
||||
/* top bar */
|
||||
.bar{display:flex;align-items:center;justify-content:space-between;gap:16px;padding-block:22px;border-bottom:1px solid var(--line)}
|
||||
.mark{font:800 1.25rem/1 var(--display);font-variation-settings:"wdth" 125;letter-spacing:-.02em;color:var(--ink);text-decoration:none;display:flex;align-items:center;gap:10px}
|
||||
.mark svg{flex:none}
|
||||
.bar nav{display:flex;gap:20px;flex-wrap:wrap;font-size:.9rem}
|
||||
.bar nav a{color:var(--muted);text-decoration:none}
|
||||
.bar nav a:hover{color:var(--ink)}
|
||||
|
||||
/* hero */
|
||||
.hero{display:grid;grid-template-columns:1.05fr .95fr;gap:48px;align-items:center;padding-block:64px 56px}
|
||||
.hero h1{font-size:clamp(2.5rem,6vw,4.4rem);line-height:.98;font-weight:800;font-variation-settings:"wdth" 125;letter-spacing:-.03em}
|
||||
.hero h1 em{font-style:normal;color:var(--fiber)}
|
||||
.lede{margin-top:22px;font-size:1.12rem;color:var(--muted);max-width:34em}
|
||||
.cta{display:flex;flex-wrap:wrap;gap:12px;margin-top:30px}
|
||||
.btn{display:inline-flex;align-items:center;gap:8px;font:600 .95rem/1 var(--body);padding:13px 18px;border-radius:8px;text-decoration:none;border:1px solid var(--fiber);background:var(--fiber);color:var(--bg)}
|
||||
.btn.ghost{background:transparent;color:var(--ink);border-color:var(--line)}
|
||||
.btn.ghost:hover{border-color:var(--ink)}
|
||||
.oneline{margin-top:26px;display:flex;align-items:center;gap:10px;background:var(--code-bg);color:var(--code-ink);border-radius:8px;padding:12px 12px 12px 16px;max-width:100%}
|
||||
.oneline code{flex:1;min-width:0;overflow-x:auto;white-space:nowrap;background:none;padding:0;font-size:.9rem;color:var(--code-ink)}
|
||||
.oneline .p{color:var(--code-dim);user-select:none}
|
||||
|
||||
/* schematic */
|
||||
figure{margin:0}
|
||||
.schem{background:var(--surface);border:1px solid var(--line);border-radius:12px;padding:18px}
|
||||
.schem svg{display:block;width:100%;height:auto}
|
||||
.schem figcaption{margin-top:12px;font-size:.85rem;color:var(--muted)}
|
||||
.s-label{font:500 11px var(--mono);fill:var(--muted)}
|
||||
.s-strong{font:600 13px var(--body);fill:var(--ink)}
|
||||
.s-box{fill:var(--bg);stroke:var(--line)}
|
||||
.s-gw{fill:var(--fiber-soft);stroke:var(--fiber);stroke-width:1.5}
|
||||
.s-fab{fill:none;stroke:var(--line);stroke-dasharray:5 4}
|
||||
.s-link{fill:none;stroke:var(--line);stroke-width:2}
|
||||
.s-gpu-a{fill:var(--fiber)} .s-gpu-b{fill:var(--om1)}
|
||||
.s-gpu-idle{fill:var(--line)}
|
||||
.pulse{fill:var(--fiber)}
|
||||
.pulse.b{fill:var(--om1)}
|
||||
@media (prefers-reduced-motion:reduce){.pulse{display:none}}
|
||||
|
||||
/* sections */
|
||||
section{padding-block:56px;border-top:1px solid var(--line)}
|
||||
.sec-head{display:grid;grid-template-columns:260px 1fr;gap:40px}
|
||||
.sec-head .intro{color:var(--muted);margin-top:12px;font-size:.95rem}
|
||||
|
||||
.facts{display:grid;grid-template-columns:1fr 1fr;gap:28px 40px}
|
||||
.fact h3{display:flex;gap:10px;align-items:baseline}
|
||||
.fact h3::before{content:"";width:10px;height:10px;border-radius:2px;background:var(--fiber);flex:none;transform:translateY(-1px)}
|
||||
.fact p{margin-top:6px;color:var(--muted);font-size:.95rem;max-width:36em}
|
||||
|
||||
/* steps */
|
||||
.steps{list-style:none;margin:0;padding:0;display:grid;gap:28px;counter-reset:s}
|
||||
.steps li{display:grid;grid-template-columns:44px 1fr;gap:4px 16px;counter-increment:s}
|
||||
.steps li>*{grid-column:2}
|
||||
.steps li::before{content:counter(s);grid-column:1;grid-row:span 4;font:800 1.5rem/1.1 var(--display);font-variation-settings:"wdth" 125;color:var(--fiber)}
|
||||
.steps p{color:var(--muted);font-size:.95rem;max-width:40em}
|
||||
.code{position:relative;margin-top:10px;background:var(--code-bg);color:var(--code-ink);border-radius:8px;min-width:0}
|
||||
.code pre{margin:0;padding:16px 18px;overflow-x:auto;line-height:1.55}
|
||||
.code .c{color:var(--code-dim)}
|
||||
.copy{position:absolute;top:8px;right:8px;font:500 .75rem/1 var(--body);padding:7px 10px;border-radius:6px;border:1px solid #2d4442;background:#1c2b2a;color:var(--code-ink);cursor:pointer}
|
||||
.copy:hover{border-color:var(--code-dim)}
|
||||
|
||||
/* nodes table in step */
|
||||
.term{margin-top:10px;background:var(--code-bg);color:var(--code-ink);border-radius:8px;overflow-x:auto}
|
||||
.term pre{margin:0;padding:14px 18px;font-size:.8rem;line-height:1.6}
|
||||
.term .ok{color:#6FD3A6}
|
||||
|
||||
/* proxy tabs */
|
||||
.tabs{min-width:0}
|
||||
.tabs input{position:absolute;opacity:0;pointer-events:none}
|
||||
.tablist{display:flex;gap:4px;border-bottom:1px solid var(--line)}
|
||||
.tablist label{cursor:pointer;padding:10px 16px;font:600 .9rem/1 var(--body);color:var(--muted);border-bottom:2px solid transparent;margin-bottom:-1px}
|
||||
#t-caddy:checked~.tablist label[for="t-caddy"],#t-nginx:checked~.tablist label[for="t-nginx"]{color:var(--ink);border-color:var(--fiber)}
|
||||
.panel{display:none;margin-top:16px}
|
||||
#t-caddy:checked~.p-caddy,#t-nginx:checked~.p-nginx{display:block}
|
||||
.panel .meta{display:flex;flex-wrap:wrap;justify-content:space-between;gap:8px 16px;font-size:.88rem;color:var(--muted)}
|
||||
.panel .code pre{max-height:460px}
|
||||
.rules{margin-top:28px;display:grid;grid-template-columns:1fr 1fr;gap:18px 32px;padding:0;list-style:none}
|
||||
.rules li{font-size:.92rem;color:var(--muted);padding-left:16px;border-left:2px solid var(--om1)}
|
||||
.rules strong{color:var(--ink);font-weight:600;display:block}
|
||||
|
||||
/* downloads */
|
||||
.dl{width:100%;border-collapse:collapse;font-size:.92rem}
|
||||
.dl-wrap{overflow-x:auto;border:1px solid var(--line);border-radius:10px;background:var(--surface)}
|
||||
.dl th{text-align:left;font:500 .72rem/1 var(--mono);letter-spacing:.1em;text-transform:uppercase;color:var(--muted);padding:14px 16px;border-bottom:1px solid var(--line)}
|
||||
.dl td{padding:12px 16px;border-bottom:1px solid var(--line);vertical-align:top}
|
||||
.dl tr:last-child td{border-bottom:0}
|
||||
.dl td:first-child{white-space:nowrap;font-weight:500}
|
||||
.dl code{background:none;padding:0;font-size:.85rem}
|
||||
.dl .note{color:var(--muted)}
|
||||
.ver{display:flex;flex-wrap:wrap;gap:10px 24px;align-items:baseline;margin-bottom:18px}
|
||||
.tag{font:500 .8rem/1 var(--mono);padding:6px 9px;border-radius:5px;background:var(--fiber-soft);color:var(--fiber)}
|
||||
|
||||
/* status */
|
||||
.status{display:grid;grid-template-columns:1fr 1fr;gap:32px}
|
||||
.status h3{margin-bottom:10px}
|
||||
.status ul{margin:0;padding:0;list-style:none;display:grid;gap:10px}
|
||||
.status li{font-size:.93rem;color:var(--muted);display:grid;grid-template-columns:18px 1fr;gap:8px}
|
||||
.status li::before{font:600 .9rem/1.6 var(--mono)}
|
||||
.proven li::before{content:"✓";color:var(--fiber)}
|
||||
.open li::before{content:"○";color:var(--om1)}
|
||||
|
||||
.next{font:500 .66rem/1 var(--mono);letter-spacing:.08em;text-transform:uppercase;padding:4px 6px;border-radius:4px;background:var(--om1-soft);color:var(--om1);white-space:nowrap;align-self:center}
|
||||
.fab-grid{display:grid;grid-template-columns:1fr;gap:28px;min-width:0}
|
||||
.term .warn{color:#E9B35A} .term .fail{color:#F07C6C} .term .dim{color:var(--code-dim)}
|
||||
.watch{display:grid;grid-template-columns:1fr 1fr;gap:18px 32px;padding:0;margin:0;list-style:none}
|
||||
.watch li{font-size:.92rem;color:var(--muted);padding-left:16px;border-left:2px solid var(--fiber)}
|
||||
.watch strong{color:var(--ink);font-weight:600;display:block}
|
||||
.watch code{font-size:.8em}
|
||||
.order{display:grid;grid-template-columns:repeat(3,1fr);gap:0;border:1px solid var(--line);border-radius:10px;background:var(--surface);overflow:hidden}
|
||||
.order div{padding:16px 18px;border-right:1px solid var(--line)}
|
||||
.order div:last-child{border-right:0}
|
||||
.order b{display:block;font:800 1.4rem/1 var(--display);font-variation-settings:"wdth" 125;color:var(--fiber);margin-bottom:8px}
|
||||
.order b.x{color:var(--om1)}
|
||||
.order h3{font-size:.95rem}
|
||||
.order p{font-size:.88rem;color:var(--muted);margin-top:4px}
|
||||
footer{border-top:1px solid var(--line);padding-block:28px 40px;display:flex;flex-wrap:wrap;justify-content:space-between;gap:12px;font-size:.85rem;color:var(--muted)}
|
||||
footer a{color:var(--muted)}
|
||||
|
||||
@media (max-width:860px){
|
||||
.hero{grid-template-columns:1fr;gap:36px;padding-block:44px 40px}
|
||||
.sec-head{grid-template-columns:1fr;gap:20px}
|
||||
.facts,.rules,.status,.watch,.order{grid-template-columns:1fr}
|
||||
.order div{border-right:0;border-bottom:1px solid var(--line)}
|
||||
.order div:last-child{border-bottom:0}
|
||||
.bar nav{display:none}
|
||||
}
|
||||
</style>
|
||||
{{end}}
|
||||
Reference in new issue
Block a user