feat: replace backend/slot toggle with role selector
The backend toggle now cycles through configured roles (chat, coder, research, distill, etc.) instead of backup model slots within the chat role. Each role uses its own primary→backup chain from the registry. - ChatRequest.slot replaced by chat_role (default "chat") - GET /backend returns available_roles instead of chat_models - _available_roles_for_toggle() builds list from defined_roles, excluding orchestrator (which has its own Agent mode) - Model label on responses now reflects the actual role's assigned model - Toggle is inert when only one role is configured (avoids useless cycling) - Add "Clear browser cache" button to Account Settings (Connected Accounts) - Add _role_model_label() helper for cleaner response tag labeling Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -33,24 +33,24 @@ def _backend_label(backend: str, username: str, role: str = "chat") -> str:
|
||||
return backend.title()
|
||||
|
||||
|
||||
def _resolve_slot_label(username: str, slot: str) -> str | None:
|
||||
"""Return the configured model label for a chat role slot, or None."""
|
||||
cfg = model_registry.get_model_for_slot(username, "chat", slot)
|
||||
def _role_model_label(username: str, role: str, actual_backend: str) -> str:
|
||||
"""Return the model label for a role, falling back to the generic backend label."""
|
||||
cfg = model_registry.get_model_for_role(username, role)
|
||||
if cfg:
|
||||
return cfg.get("label") or cfg.get("model_name")
|
||||
return None
|
||||
return cfg.get("label") or cfg.get("model_name") or _backend_label(actual_backend, username, role)
|
||||
return _backend_label(actual_backend, username, role)
|
||||
|
||||
|
||||
class ChatRequest(BaseModel):
|
||||
message: str
|
||||
session_id: str | None = None
|
||||
tier: int | None = None
|
||||
model: str | None = None # legacy backend override ("claude"|"gemini"|"local")
|
||||
slot: str | None = None # Phase 3: role slot ("primary"|"backup_1"|"backup_2")
|
||||
model: str | None = None # legacy backend override ("claude"|"gemini"|"local")
|
||||
chat_role: str = "chat" # active role: "chat"|"coder"|"research"|"distill" etc.
|
||||
include_long: bool = True
|
||||
include_mid: bool = True
|
||||
include_short: bool = True
|
||||
off_record: bool = False # skip session log (in-memory context preserved)
|
||||
off_record: bool = False # skip session log (in-memory context preserved)
|
||||
user: str = "scott"
|
||||
persona: str = "inara"
|
||||
|
||||
@@ -103,7 +103,7 @@ async def _stream_chat(req: ChatRequest):
|
||||
system_prompt=system_prompt,
|
||||
messages=history,
|
||||
model=req.model,
|
||||
slot=req.slot,
|
||||
role=req.chat_role,
|
||||
))
|
||||
|
||||
try:
|
||||
@@ -119,11 +119,7 @@ async def _stream_chat(req: ChatRequest):
|
||||
|
||||
try:
|
||||
response_text, actual_backend = task.result()
|
||||
# Use the slot's model label when a slot was pinned; fall back to generic label
|
||||
if req.slot:
|
||||
backend_label = _resolve_slot_label(user, req.slot) or _backend_label(actual_backend, user)
|
||||
else:
|
||||
backend_label = _backend_label(actual_backend, user, role="chat")
|
||||
backend_label = _role_model_label(user, req.chat_role, actual_backend)
|
||||
host = platform.node()
|
||||
history.append({
|
||||
"role": "assistant",
|
||||
@@ -201,32 +197,38 @@ def _local_model_info(request: Request) -> dict | None:
|
||||
return None
|
||||
|
||||
|
||||
def _chat_models_for_toggle(username: str) -> list[dict]:
|
||||
"""Return non-empty chat role slots as [{slot, label, type}] for the UI toggle."""
|
||||
def _available_roles_for_toggle(username: str) -> list[dict]:
|
||||
"""Return roles with a primary model assigned (excluding orchestrator) for the UI toggle.
|
||||
|
||||
Returns [{role, label, model_label, type}] ordered by settings.defined_roles.
|
||||
"""
|
||||
registry = model_registry.get_registry(username)
|
||||
role_cfg = registry.get("roles", {}).get("chat", {})
|
||||
roles_cfg = registry.get("roles", {})
|
||||
result = []
|
||||
for slot in model_registry.PRIORITY_KEYS[:3]:
|
||||
model_id = role_cfg.get(slot)
|
||||
if not model_id:
|
||||
for role_name in settings.get_defined_roles():
|
||||
if role_name == "orchestrator":
|
||||
continue
|
||||
resolved = model_registry._resolve_model(registry, model_id)
|
||||
primary_id = roles_cfg.get(role_name, {}).get("primary")
|
||||
if not primary_id:
|
||||
continue
|
||||
resolved = model_registry._resolve_model(registry, primary_id)
|
||||
if resolved:
|
||||
result.append({
|
||||
"slot": slot,
|
||||
"label": resolved.get("label") or resolved.get("model_name") or slot,
|
||||
"type": resolved.get("type", ""),
|
||||
"role": role_name,
|
||||
"label": role_name.title(),
|
||||
"model_label": resolved.get("label") or resolved.get("model_name") or "",
|
||||
"type": resolved.get("type", ""),
|
||||
})
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/backend")
|
||||
async def get_backend(request: Request) -> dict:
|
||||
username = _request_user(request)
|
||||
chat_models = _chat_models_for_toggle(username) if username else []
|
||||
username = _request_user(request)
|
||||
available_roles = _available_roles_for_toggle(username) if username else []
|
||||
p = settings.primary_backend
|
||||
return {
|
||||
"chat_models": chat_models,
|
||||
"available_roles": available_roles,
|
||||
# Legacy fields kept for backward compat
|
||||
"primary": p,
|
||||
"fallback": _BACKEND_FALLBACK.get(p, "claude"),
|
||||
|
||||
Reference in New Issue
Block a user