From d998ea7ef55a25dfd7a30aa220485d8a6562cb36 Mon Sep 17 00:00:00 2001
From: Ilker Manap
Date: Sun, 27 Sep 2026 16:51:09 +0200
Subject: [PATCH] Let a model say what it is for
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
A gateway serving nine models offered nine names and nothing to choose between
them. /v1/models is the only view most clients ever have, and it listed ids.
Models carry a description now: written when the model is added, shown on the
dashboard and sent to clients in /v1/models.
Written rather than derived. A registry offers a pipeline tag and a wall of
README, and neither answers the question somebody picking from a list is
asking — which of these should I send this request to. "General chat, strong
Swedish; slower than the 8B" is not in any metadata field.
It is optional, and absent means absent rather than an empty string in the
response: a model nobody described should read as undescribed, not as described
with nothing.
On the dashboard it sits under its row rather than in a column. A sentence in a
table cell either wraps every other column into a stripe or gets cut off.
Co-Authored-By: Claude Opus 5 (1M context)
---
docs/reference.md | 16 +++++++++++++
internal/control/adminapi/api.go | 22 ++++++++++--------
internal/control/catalog/catalog.go | 11 +++++++++
internal/router/handlers.go | 6 +++++
internal/web/admin_serve.go | 1 +
internal/web/admin_serve_test.go | 29 ++++++++++++++++++++++++
internal/web/templates/admin_mirror.html | 6 +++++
internal/web/templates/dashboard.html | 6 ++++-
8 files changed, 86 insertions(+), 11 deletions(-)
diff --git a/docs/reference.md b/docs/reference.md
index 06dfb008..9ae7312e 100644
--- a/docs/reference.md
+++ b/docs/reference.md
@@ -84,6 +84,7 @@ rather than repeated.
| `timeout` | `time.Duration` | [Mirror](#mirror) |
| `name` | `string` | [Model](#model) |
| `aliases` | `[]string` | [Model](#model) |
+| `description` | `string` | [Model](#model) |
| `source` | `string` | [Model](#model) |
| `profiles` | `[]Profile` | [Model](#model) |
| `scaling` | `Scaling` | [Model](#model) |
@@ -632,6 +633,21 @@ arriving long after this deadline has passed.
`[]string`
+### `description`
+
+`string`
+
+Description is what this model is for, in a line.
+
+Written by whoever added it, because nobody else can say it. A registry
+offers a pipeline tag and a wall of README, and neither answers the
+question a person picking from a list is asking: which of these nine
+names should I send this request to.
+
+It reaches clients through /v1/models, which is the only view most of
+them ever see. A gateway serving nine models by name alone makes that
+choice by guesswork.
+
### `source`
`string`
diff --git a/internal/control/adminapi/api.go b/internal/control/adminapi/api.go
index 9303f572..97747687 100644
--- a/internal/control/adminapi/api.go
+++ b/internal/control/adminapi/api.go
@@ -108,15 +108,17 @@ type NodeView struct {
// ModelView is a model as `ctl models` shows it.
type ModelView struct {
- Name string `json:"name"`
- Aliases []string `json:"aliases,omitempty"`
- Source string `json:"source"`
- Min int `json:"min_replicas"`
- Max int `json:"max_replicas"`
- Ready int `json:"ready_replicas"`
- Total int `json:"total_replicas"`
- Fallbacks []string `json:"fallbacks,omitempty"`
- Profiles []string `json:"profiles"`
+ Name string `json:"name"`
+ // Description is what the model is for, when somebody said.
+ Description string `json:"description,omitempty"`
+ Aliases []string `json:"aliases,omitempty"`
+ Source string `json:"source"`
+ Min int `json:"min_replicas"`
+ Max int `json:"max_replicas"`
+ Ready int `json:"ready_replicas"`
+ Total int `json:"total_replicas"`
+ Fallbacks []string `json:"fallbacks,omitempty"`
+ Profiles []string `json:"profiles"`
}
// ReplicaView is a replica as `ctl replicas` shows it.
@@ -260,7 +262,7 @@ func (a *API) Models() []ModelView {
out := make([]ModelView, 0)
for _, m := range a.ctl.Catalog().Models() {
v := ModelView{
- Name: m.Name, Aliases: m.Aliases, Source: m.Source,
+ Name: m.Name, Description: m.Description, Aliases: m.Aliases, Source: m.Source,
Min: m.Scaling.Min, Max: m.Scaling.Max,
Ready: ready[m.Name], Total: total[m.Name],
Fallbacks: m.Fallbacks,
diff --git a/internal/control/catalog/catalog.go b/internal/control/catalog/catalog.go
index 644fbff7..0750920e 100644
--- a/internal/control/catalog/catalog.go
+++ b/internal/control/catalog/catalog.go
@@ -123,6 +123,17 @@ func cpuCapable(engine string) bool { return engine == "llamacpp" }
type Model struct {
Name string `json:"name,omitempty" koanf:"name"`
Aliases []string `json:"aliases,omitempty" koanf:"aliases"`
+ // Description is what this model is for, in a line.
+ //
+ // Written by whoever added it, because nobody else can say it. A registry
+ // offers a pipeline tag and a wall of README, and neither answers the
+ // question a person picking from a list is asking: which of these nine
+ // names should I send this request to.
+ //
+ // It reaches clients through /v1/models, which is the only view most of
+ // them ever see. A gateway serving nine models by name alone makes that
+ // choice by guesswork.
+ Description string `json:"description,omitempty" koanf:"description"`
// Source is the weights path or a HuggingFace id.
Source string `json:"source,omitempty" koanf:"source"`
Profiles []Profile `json:"profiles,omitempty" koanf:"profiles"`
diff --git a/internal/router/handlers.go b/internal/router/handlers.go
index f1effbfc..5f7091d0 100644
--- a/internal/router/handlers.go
+++ b/internal/router/handlers.go
@@ -715,6 +715,12 @@ func (r *Router) handleModels(w http.ResponseWriter, _ *http.Request) {
if m.MaxOutput > 0 {
entry["max_output_tokens"] = m.MaxOutput
}
+ // Not part of the OpenAI shape, and the reason to send it anyway is
+ // that this list is the only view most clients have: nine names and
+ // nothing to choose between them.
+ if m.Description != "" {
+ entry["description"] = m.Description
+ }
entry["capabilities"] = map[string]bool{
"tools": m.Capabilities.Tools, "vision": m.Capabilities.Vision,
"embeddings": m.Capabilities.Embeddings,
diff --git a/internal/web/admin_serve.go b/internal/web/admin_serve.go
index a583068d..ea7836d3 100644
--- a/internal/web/admin_serve.go
+++ b/internal/web/admin_serve.go
@@ -169,6 +169,7 @@ func gpus(fleet Source) int {
func buildModel(get func(string) string) (catalog.Model, error) {
m := catalog.Model{
Name: strings.TrimSpace(get("name")),
+ Description: strings.TrimSpace(get("description")),
Source: strings.TrimSpace(get("source")),
RoutingProfile: "chat",
}
diff --git a/internal/web/admin_serve_test.go b/internal/web/admin_serve_test.go
index 97f6b8fd..e0fb9475 100644
--- a/internal/web/admin_serve_test.go
+++ b/internal/web/admin_serve_test.go
@@ -295,3 +295,32 @@ func TestAnEstimateIsNotMadeWhenTheSizeIsUnknown(t *testing.T) {
t.Errorf("an unknown size should leave no figure; got %s", body)
}
}
+
+func TestADescriptionReachesTheCatalog(t *testing.T) {
+ cat := &fakeCatalog{}
+ mirrorPost(t, servePage(t, nil, cat, 0), "/admin/mirror/serve", url.Values{
+ "csrf": {"tok"}, "name": {"coder"}, "source": {"http://x/m.gguf"},
+ "engine": {"llamacpp"}, "replicas": {"1"}, "context_length": {"4096"},
+ "description": {"code completion and review; 7B, fits the large node"},
+ })
+
+ if len(cat.added) != 1 {
+ t.Fatalf("expected one model, got %d", len(cat.added))
+ }
+ // The line a client sees in /v1/models, which for most of them is the only
+ // view of what this gateway serves.
+ if cat.added[0].Description != "code completion and review; 7B, fits the large node" {
+ t.Errorf("the description was lost: %q", cat.added[0].Description)
+ }
+}
+
+func TestAModelWithNoDescriptionIsStillAccepted(t *testing.T) {
+ cat := &fakeCatalog{}
+ mirrorPost(t, servePage(t, nil, cat, 0), "/admin/mirror/serve", url.Values{
+ "csrf": {"tok"}, "name": {"coder"}, "source": {"http://x/m.gguf"},
+ "engine": {"llamacpp"}, "replicas": {"1"}, "context_length": {"4096"},
+ })
+ if len(cat.added) != 1 || cat.added[0].Description != "" {
+ t.Errorf("a description is optional: %+v", cat.added)
+ }
+}
diff --git a/internal/web/templates/admin_mirror.html b/internal/web/templates/admin_mirror.html
index 5bde8acc..0d4fbbce 100644
--- a/internal/web/templates/admin_mirror.html
+++ b/internal/web/templates/admin_mirror.html
@@ -100,6 +100,12 @@ reachable from this machine.
+ {{/* Reaches clients through /v1/models, which for most of them is the only
+ view of what this gateway serves. Nine names and nothing to choose
+ between them is a choice made by guesswork. */}}
+
diff --git a/internal/web/templates/dashboard.html b/internal/web/templates/dashboard.html
index b8ecd327..bcd9d942 100644
--- a/internal/web/templates/dashboard.html
+++ b/internal/web/templates/dashboard.html
@@ -129,7 +129,11 @@ requested whenever a fallback served it.
{{.Min}}–{{.Max}}
{{join .Profiles}}
{{join .Fallbacks}}
-{{end}}
+
+{{/* Under the row rather than in a column: a sentence in a table cell either
+ wraps every other column into a stripe or gets cut off. */}}
+{{if .Description}}