CI / test (push) Successful in 1m31s
big-model runs on the sim engine, whose whole vocabulary is the Greek alphabet, cycled at a fixed rate until max_tokens. That is what it is for: with no H100s it is the only thing exercising the TP8 two-node path, the fabric invariant and rebuild-after-node-loss. The output is the point. What was wrong is that nothing said so. /v1/models returned it with owned_by "llmgw", identical to the real model, and the chat picker sorted purely by name — so "big-model" led the list, was preselected by the browser, and anyone who typed a question and pressed Send got Greek letters back with no explanation. Reported as the model spitting out crap, which is a fair reading of what it looks like from outside. Simulated is derived from the profiles rather than configured: an operator who adds a sim profile and forgets a flag would be shipping placeholder text under a real model's name. A model counts as simulated only when every profile is, because one real profile means the request can still come back as inference. The marker on /v1/models is owned_by "llmgw-sim", which is already what the sim engine calls itself on its own /v1/models — the gateway was flattening a distinction that existed one layer down. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
75 lines
2.1 KiB
Go
75 lines
2.1 KiB
Go
package web
|
|
|
|
import (
|
|
"net/http"
|
|
"time"
|
|
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/store"
|
|
)
|
|
|
|
// ChatModel is one entry in the chat page's model picker.
|
|
type ChatModel struct {
|
|
Name string
|
|
// Simulated marks a model that returns synthetic text rather than
|
|
// inference output. The picker says so, because the two are
|
|
// indistinguishable from the outside until the reply arrives.
|
|
Simulated bool
|
|
}
|
|
|
|
type chatData struct {
|
|
User store.User
|
|
Models []ChatModel
|
|
CSRF string
|
|
Version string
|
|
Now string
|
|
}
|
|
|
|
// Chat renders the in-browser chat for one signed-in person.
|
|
func Chat(user store.User, models []ChatModel, version string) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
noStore(w)
|
|
render(w, "chat", chatData{
|
|
User: user, Models: models, CSRF: csrfFor(r),
|
|
Version: version, Now: time.Now().UTC().Format(time.RFC3339),
|
|
})
|
|
}
|
|
}
|
|
|
|
// Guard runs a handler only for a signed-in, approved account.
|
|
//
|
|
// Exported because the chat lives in the router — it needs the endpoint
|
|
// registry and the request path — while the session machinery lives here. The
|
|
// alternative was a second copy of the session check next to the one thing that
|
|
// must never have two copies.
|
|
//
|
|
// Its POSTs carry the CSRF token in a header rather than a form field, because
|
|
// they are JSON. A header is a stronger check than a field: a cross-site form
|
|
// post cannot set one at all.
|
|
func (p *AccountPages) Guard(
|
|
next func(w http.ResponseWriter, r *http.Request, user store.User),
|
|
) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
noStore(w)
|
|
sess, user, ok := p.session(r)
|
|
if !ok {
|
|
if r.Method == http.MethodGet {
|
|
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
|
} else {
|
|
http.Error(w, "not signed in", http.StatusUnauthorized)
|
|
}
|
|
return
|
|
}
|
|
if r.Method != http.MethodGet {
|
|
token := r.Header.Get("X-CSRF-Token")
|
|
if token == "" {
|
|
token = r.FormValue("csrf")
|
|
}
|
|
if !p.accounts.CheckCSRF(sess, token) {
|
|
http.Error(w, "bad request", http.StatusBadRequest)
|
|
return
|
|
}
|
|
}
|
|
next(w, r, user)
|
|
}
|
|
}
|