diff --git a/routes/cookbook_routes.py b/routes/cookbook_routes.py
index 408693cb3..7d9e6e0cb 100644
--- a/routes/cookbook_routes.py
+++ b/routes/cookbook_routes.py
@@ -1416,6 +1416,7 @@ def setup_cookbook_routes() -> APIRouter:
"""Register a diffusion model as an image endpoint so it appears in the model selector."""
import re
from core.database import SessionLocal, ModelEndpoint
+ from routes.model_routes import _normalize_model_ids
from src.settings import load_settings, save_settings
# Parse port from command (--port NNNN), default 8100 for diffusion_server
@@ -1443,7 +1444,10 @@ def setup_cookbook_routes() -> APIRouter:
if existing:
existing.is_enabled = True
existing.model_type = "image"
- existing.name = display_name
+ # Follow the newly served model unless the user renamed the endpoint.
+ pinned_names = {f"{m.split('/')[-1]} (image)" for m in _normalize_model_ids(existing.pinned_models)}
+ if existing.name in pinned_names:
+ existing.name = display_name
existing.endpoint_kind = "local"
existing.model_refresh_mode = "manual"
existing.cookbook_serve_session_id = serve_session_id
@@ -1772,7 +1776,10 @@ def setup_cookbook_routes() -> APIRouter:
if existing:
existing.is_enabled = True
existing.model_type = "llm"
- existing.name = display_name
+ # Follow the newly served model unless the user renamed the endpoint.
+ pinned_names = {m.split("/")[-1] for m in _normalize_model_ids(existing.pinned_models)}
+ if existing.name in pinned_names:
+ existing.name = display_name
existing.endpoint_kind = "local"
existing.model_refresh_mode = "auto"
# A reused endpoint belongs to the newest serve session.
diff --git a/static/css/06-admin-settings.css b/static/css/06-admin-settings.css
index 788470329..9869ef5e5 100644
--- a/static/css/06-admin-settings.css
+++ b/static/css/06-admin-settings.css
@@ -52,6 +52,45 @@
gap: 8px;
}
.admin-user-name { font-size: 13px; font-weight: 500; }
+/* Inline endpoint rename: the input keeps the name's typography so the row doesn't shift */
+.adm-ep-rename-btn {
+ background: none;
+ border: none;
+ padding: 0 2px;
+ color: inherit;
+ line-height: 1;
+ cursor: pointer;
+ opacity: 0.45;
+}
+.adm-ep-rename-btn:hover,
+.adm-ep-rename-btn:focus-visible { opacity: 0.9; }
+@media (hover: hover) {
+ .adm-ep-rename-btn { opacity: 0; }
+ .admin-user-row:hover .adm-ep-rename-btn { opacity: 0.45; }
+ .admin-user-row .adm-ep-rename-btn:hover,
+ .adm-ep-rename-btn:focus-visible { opacity: 0.9; }
+}
+.admin-user-row input.adm-ep-rename-input {
+ width: auto;
+ field-sizing: content;
+ max-width: 100%;
+ height: auto;
+ padding: 0 0 1px;
+ line-height: inherit;
+ color: inherit;
+ background: transparent;
+ border: none;
+ border-bottom: 1px solid var(--red);
+ border-radius: 0;
+ outline: none;
+}
+.admin-user-info:has(.adm-ep-name, .adm-ep-rename-input) { min-width: 0; }
+.adm-ep-name {
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
/* Privilege panel */
.admin-priv-panel {
max-height: 600px;
diff --git a/static/js/admin.js b/static/js/admin.js
index 375c3d1cf..1f42208d1 100644
--- a/static/js/admin.js
+++ b/static/js/admin.js
@@ -1074,7 +1074,8 @@ async function loadEndpoints() {
` : ''}
${providerLogoFromUrl(ep.base_url) || ''}
- ${esc(epTitle)}
+ ${esc(epTitle)}
+ ${_endpointSelectMode || (isChatgptAccount && ep.account_label) ? '' : ``}
${ep.model_type === 'image' ? 'Image' : ''}
${kindLabel ? `${esc(kindLabel)}` : ''}
${statusBadge}
@@ -1204,6 +1205,70 @@ async function loadEndpoints() {
loadEndpoints();
});
});
+ queryAll('[data-adm-rename-ep]').forEach(btn => {
+ btn.addEventListener('click', (e) => {
+ e.stopPropagation();
+ const nameEl = btn.previousElementSibling;
+ if (!nameEl || !nameEl.classList.contains('admin-user-name')) return;
+ const original = btn.dataset.admEpTitle || '';
+ const input = document.createElement('input');
+ input.type = 'text';
+ input.value = original;
+ input.placeholder = 'Endpoint name';
+ input.setAttribute('aria-label', 'Endpoint name');
+ input.maxLength = 64;
+ input.autocomplete = 'off';
+ input.setAttribute('data-bwignore', '');
+ input.className = 'admin-user-name adm-ep-rename-input';
+ input.style.minWidth = `min(${nameEl.getBoundingClientRect().width}px, 100%)`;
+ nameEl.replaceWith(input);
+ btn.style.visibility = 'hidden';
+ input.focus();
+ input.select();
+ let done = false;
+ const restore = () => {
+ input.replaceWith(nameEl);
+ btn.style.visibility = '';
+ };
+ const commit = async () => {
+ if (done) return;
+ done = true;
+ const name = input.value.trim();
+ if (!name || name === original) { restore(); return; }
+ const epId = btn.dataset.admRenameEp;
+ if (data.some(ep => String(ep.id) !== epId && (ep.name || '').trim().toLowerCase() === name.toLowerCase())) {
+ uiModule.showError(`An endpoint named "${name}" already exists`);
+ done = false;
+ input.focus();
+ return;
+ }
+ input.disabled = true;
+ try {
+ const res = await fetch(`/api/model-endpoints/${encodeURIComponent(epId)}`, {
+ method: 'PATCH',
+ credentials: 'same-origin',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ name }),
+ });
+ if (!res.ok) {
+ const data = await res.json().catch(() => ({}));
+ throw new Error(data.detail || 'Failed to rename endpoint');
+ }
+ } catch (err) {
+ uiModule.showError(err.message || 'Failed to rename endpoint');
+ restore();
+ return;
+ }
+ await _refreshAfterEndpointChange();
+ loadEndpoints();
+ };
+ input.addEventListener('blur', commit);
+ input.addEventListener('keydown', (ev) => {
+ if (ev.key === 'Enter') { ev.preventDefault(); input.blur(); }
+ if (ev.key === 'Escape') { ev.preventDefault(); ev.stopPropagation(); done = true; restore(); }
+ });
+ });
+ });
queryAll('[data-adm-copy-url]').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
diff --git a/tests/test_cookbook_endpoint_registration.py b/tests/test_cookbook_endpoint_registration.py
index f413eff25..802fbbf0c 100644
--- a/tests/test_cookbook_endpoint_registration.py
+++ b/tests/test_cookbook_endpoint_registration.py
@@ -445,3 +445,83 @@ async def test_ordinary_admin_delete_does_not_require_serve_owner(
response = await client.delete(f"http://testserver/api/model-endpoints/{endpoint_id}")
assert response.status_code == 200, response.text
assert _endpoint(endpoint_db, endpoint_id) is None
+
+
+def _launch_image(repo_id: str, port: int, monkeypatch) -> str:
+ import routes.cookbook_routes as cookbook_routes
+ import src.settings as settings
+ from routes.cookbook_helpers import ServeRequest
+
+ monkeypatch.setattr(settings, "load_settings", lambda: {"image_gen_enabled": True, "image_model": repo_id})
+ monkeypatch.setattr(settings, "save_settings", lambda _settings: None)
+ router = cookbook_routes.setup_cookbook_routes()
+ serve = next(
+ route.endpoint for route in router.routes
+ if route.path == "/api/model/serve" and "POST" in route.methods
+ )
+ cells = dict(zip(serve.__code__.co_freevars, serve.__closure__ or ()))
+ register = cells["_auto_register_image_endpoint"].cell_contents
+ endpoint_id = register(
+ ServeRequest(repo_id=repo_id, cmd=f"python diffusion_server.py --model {repo_id} --port {port}"), None
+ )
+ assert endpoint_id
+ return endpoint_id
+
+
+def _rename(factory, endpoint_id: str, name: str) -> None:
+ import core.database as database
+
+ with factory() as db:
+ db.get(database.ModelEndpoint, endpoint_id).name = name
+ db.commit()
+
+
+@pytest.mark.parametrize("relaunched", ["Qwen/Qwen3-8B", "meta-llama/Llama-3.1-8B"])
+def test_relaunch_keeps_renamed_endpoint_name(endpoint_db, relaunched):
+ endpoint = _launch_llm("Qwen/Qwen3-8B", 8001)
+ _rename(endpoint_db, endpoint, "Workstation GPU")
+
+ assert _launch_llm(relaunched, 8001) == endpoint
+
+ assert _endpoint(endpoint_db, endpoint).name == "Workstation GPU"
+
+
+def test_relaunch_names_endpoint_after_served_model_when_not_renamed(endpoint_db):
+ endpoint = _launch_llm("Qwen/Qwen3-8B", 8001)
+
+ for repo_id, name in [
+ ("meta-llama/Llama-3.1-8B", "Llama-3.1-8B"),
+ ("Qwen/Qwen3-8B", "Qwen3-8B"),
+ ("meta-llama/Llama-3.1-8B", "Llama-3.1-8B"),
+ ]:
+ _launch_llm(repo_id, 8001)
+ assert _endpoint(endpoint_db, endpoint).name == name
+
+
+def test_launch_keeps_name_of_endpoint_without_pinned_models(endpoint_db):
+ import core.database as database
+
+ with endpoint_db() as db:
+ db.add(database.ModelEndpoint(id="manual", name="My vLLM box", base_url="http://localhost:8001/v1"))
+ db.commit()
+
+ assert _launch_llm("Qwen/Qwen3-8B", 8001) == "manual"
+
+ assert _endpoint(endpoint_db, "manual").name == "My vLLM box"
+
+
+def test_image_relaunch_keeps_renamed_endpoint_name(endpoint_db, monkeypatch):
+ endpoint = _launch_image("black-forest-labs/FLUX.1-schnell", 8100, monkeypatch)
+ _rename(endpoint_db, endpoint, "Image box")
+
+ _launch_image("stabilityai/sdxl-turbo", 8100, monkeypatch)
+
+ assert _endpoint(endpoint_db, endpoint).name == "Image box"
+
+
+def test_image_relaunch_names_endpoint_after_served_model_when_not_renamed(endpoint_db, monkeypatch):
+ endpoint = _launch_image("black-forest-labs/FLUX.1-schnell", 8100, monkeypatch)
+
+ _launch_image("stabilityai/sdxl-turbo", 8100, monkeypatch)
+
+ assert _endpoint(endpoint_db, endpoint).name == "sdxl-turbo (image)"