Support in-place endpoint updates and recover empty-model sessions (#786)

The "don't wipe endpoint_url/model on endpoint delete" half of #587 landed
in 6a78b02 (Fix endpoint model preservation for tasks). The three remaining
follow-up pieces from the original PR — flagged in the review on #786 —
are:

- routes/model_routes.py: toggle_model_endpoint (PATCH) now accepts
  api_key and base_url, so the admin UI can rotate a key or fix a typo'd
  URL without going through delete+recreate. base_url is normalized the
  same way the POST handler does (strip /models, /chat/completions,
  /completions, /v1/messages, then _normalize_base). Cache invalidation
  matches the POST/DELETE paths and the response includes base_url so the
  frontend can confirm what was saved.

- routes/chat_routes.py: new _recover_empty_session_model picks
  cached_models[0] from the endpoint that matches sess.endpoint_url and
  persists it onto the Session row before the LLM call goes out. Wired
  into both /api/chat and /api/chat_stream after the existing
  _clear_orphaned_session_endpoint guard, so the order is: drop
  truly-orphaned sessions first, then heal the "picker showed it, session
  never knew" case.

- routes/chat_routes.py: when recovery fails (no endpoint, no cached
  models) raise HTTP 400 with a clear message instead of letting
  model="" reach the upstream as 401/503.

Closes #587.
This commit is contained in:
tanmayraut45
2026-06-02 07:56:38 +05:30
committed by GitHub
parent 63a947d246
commit 0e31c38be0
2 changed files with 99 additions and 0 deletions
+18
View File
@@ -1294,16 +1294,34 @@ def setup_model_routes(model_discovery):
ep.name = body["name"].strip() or ep.name
if "model_type" in body and isinstance(body["model_type"], str):
ep.model_type = body["model_type"].strip() or ep.model_type
# Rotating an API key used to require DELETE+POST, which wiped
# endpoint_url/model from every session referencing the old base
# URL. Allow in-place updates so the admin can change the key
# (or correct a typo'd base URL) without nuking session state.
if "api_key" in body and isinstance(body["api_key"], str):
_new_key = body["api_key"].strip()
# Empty string means "clear it" (e.g. local Ollama no longer needs a key).
ep.api_key = _new_key or None
if "base_url" in body and isinstance(body["base_url"], str):
_new_base = body["base_url"].strip().rstrip("/")
for _suffix in ("/models", "/chat/completions", "/completions", "/v1/messages"):
if _new_base.endswith(_suffix):
_new_base = _new_base[: -len(_suffix)].rstrip("/")
_new_base = _normalize_base(_new_base)
if _new_base:
ep.base_url = _new_base
else:
ep.is_enabled = not ep.is_enabled
db.commit()
_invalidate_models_cache()
_local_probe_cache["data"] = None
return {
"id": ep.id,
"is_enabled": ep.is_enabled,
"supports_tools": ep.supports_tools,
"name": ep.name,
"model_type": ep.model_type,
"base_url": ep.base_url,
}
finally:
db.close()