fix: backend toggle not sent to server; add per-message model tag
Fixes:
- app.js was tracking primaryBackend locally but never included
model: primaryBackend in the /chat POST body, so the server always
used settings.primary_backend regardless of what the user clicked.
Now model: primaryBackend is sent on every chat request.
- Responses were only annotated when fallback occurred. Now every
assistant message shows a small model tag at the bottom right.
chat.py:
- _backend_label() resolves human-readable name:
claude → "Claude", gemini → "Gemini",
local → registry label (e.g. "Gemma 4 E4B") or model_name
- SSE payload now includes backend_label field
app.js:
- model: primaryBackend added to /chat fetch body
- After every response, appends .model-tag div with backend_label
- Fallback shows "⚡ fallback → <label>" in amber; normal is muted
- Removed separate system message for fallback (tag covers it)
style.css:
- .model-tag: small muted text, right-aligned, separated by thin line
- .model-tag.fallback: amber (#f59e0b)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,20 @@ import event_bus
|
|||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
def _backend_label(backend: str, username: str) -> str:
|
||||||
|
"""Human-readable label for the model that handled a request."""
|
||||||
|
if backend == "claude":
|
||||||
|
return "Claude"
|
||||||
|
if backend == "gemini":
|
||||||
|
return "Gemini"
|
||||||
|
if backend == "local":
|
||||||
|
cfg = model_registry.get_best_local_model(username)
|
||||||
|
if cfg:
|
||||||
|
return cfg.get("label") or cfg.get("model_name") or "Local"
|
||||||
|
return "Local"
|
||||||
|
return backend.title()
|
||||||
|
|
||||||
|
|
||||||
class ChatRequest(BaseModel):
|
class ChatRequest(BaseModel):
|
||||||
message: str
|
message: str
|
||||||
session_id: str | None = None
|
session_id: str | None = None
|
||||||
@@ -105,6 +119,7 @@ async def _stream_chat(req: ChatRequest):
|
|||||||
"response": response_text,
|
"response": response_text,
|
||||||
"session_id": session_id,
|
"session_id": session_id,
|
||||||
"backend": actual_backend,
|
"backend": actual_backend,
|
||||||
|
"backend_label": _backend_label(actual_backend, user),
|
||||||
"fallback_used": actual_backend != requested,
|
"fallback_used": actual_backend != requested,
|
||||||
}
|
}
|
||||||
yield f"data: {json.dumps(payload)}\n\n"
|
yield f"data: {json.dumps(payload)}\n\n"
|
||||||
|
|||||||
@@ -949,6 +949,7 @@
|
|||||||
include_mid: memMid,
|
include_mid: memMid,
|
||||||
include_short: memShort,
|
include_short: memShort,
|
||||||
off_record: current_mode === 'otr',
|
off_record: current_mode === 'otr',
|
||||||
|
model: primaryBackend,
|
||||||
user: CORTEX_USER,
|
user: CORTEX_USER,
|
||||||
persona: CORTEX_PERSONA,
|
persona: CORTEX_PERSONA,
|
||||||
}),
|
}),
|
||||||
@@ -984,10 +985,15 @@
|
|||||||
const assistHistIdx = currentHistory.length;
|
const assistHistIdx = currentHistory.length;
|
||||||
currentHistory.push({ role: 'assistant', content: data.response });
|
currentHistory.push({ role: 'assistant', content: data.response });
|
||||||
attachHistoryControls(thinkingDiv, assistHistIdx);
|
attachHistoryControls(thinkingDiv, assistHistIdx);
|
||||||
if (data.fallback_used) {
|
|
||||||
addMessage('system',
|
// Model tag — always shown, amber if fallback was used
|
||||||
`⚡ ${primaryBackend} unavailable — answered by ${data.backend}`);
|
const modelTag = document.createElement('div');
|
||||||
}
|
modelTag.className = 'model-tag' + (data.fallback_used ? ' fallback' : '');
|
||||||
|
const label = data.backend_label || data.backend || '';
|
||||||
|
modelTag.textContent = data.fallback_used
|
||||||
|
? `⚡ fallback → ${label}`
|
||||||
|
: label;
|
||||||
|
thinkingDiv.appendChild(modelTag);
|
||||||
} else if (data.type === 'error') {
|
} else if (data.type === 'error') {
|
||||||
throw new Error(data.message);
|
throw new Error(data.message);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -552,6 +552,20 @@
|
|||||||
.copy-btn:hover { color: var(--text); border-color: var(--muted); }
|
.copy-btn:hover { color: var(--text); border-color: var(--muted); }
|
||||||
.copy-btn.copied { color: var(--success); border-color: var(--success-dim); }
|
.copy-btn.copied { color: var(--success); border-color: var(--success-dim); }
|
||||||
|
|
||||||
|
/* Model tag — shown at the bottom of every assistant message */
|
||||||
|
.model-tag {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.67rem;
|
||||||
|
color: #334155;
|
||||||
|
margin-top: 0.55rem;
|
||||||
|
padding-top: 0.4rem;
|
||||||
|
border-top: 1px solid #1e2030;
|
||||||
|
text-align: right;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.model-tag.fallback { color: #f59e0b; }
|
||||||
|
|
||||||
/* Note messages */
|
/* Note messages */
|
||||||
.message.note-private {
|
.message.note-private {
|
||||||
align-self: flex-end;
|
align-self: flex-end;
|
||||||
|
|||||||
Reference in New Issue
Block a user