feat: store and display backend + host metadata on chat messages

Each assistant message in the session JSON now carries:
  backend, backend_label, host (platform.node())

These fields are shown as model tags in the UI — on live responses and
when loading session history. Session log entries (sessions/YYYY-MM-DD.md)
include the backend label and host in the turn header.

The local (OpenAI-compat) backend strips non-standard fields before
sending messages to the API so extra fields don't leak upstream.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-04-08 22:16:48 -04:00
parent 6c84d6ae72
commit 3b3456600a
4 changed files with 36 additions and 8 deletions

View File

@@ -183,7 +183,8 @@ async def _local(system_prompt: str, messages: list[dict], model_cfg: dict | Non
msgs: list[dict] = [] msgs: list[dict] = []
if system_prompt: if system_prompt:
msgs.append({"role": "system", "content": system_prompt}) msgs.append({"role": "system", "content": system_prompt})
msgs.extend(messages) # Strip any non-standard metadata fields before sending to the API
msgs.extend({"role": m["role"], "content": m["content"]} for m in messages)
url = api_url.rstrip("/") + chat_path url = api_url.rstrip("/") + chat_path
headers: dict[str, str] = {} headers: dict[str, str] = {}

View File

@@ -1,5 +1,6 @@
import asyncio import asyncio
import json import json
import platform
import jwt import jwt
from fastapi import APIRouter, HTTPException, Query, Request from fastapi import APIRouter, HTTPException, Query, Request
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
@@ -108,10 +109,18 @@ async def _stream_chat(req: ChatRequest):
try: try:
response_text, actual_backend = task.result() response_text, actual_backend = task.result()
history.append({"role": "assistant", "content": response_text}) backend_label = _backend_label(actual_backend, user, role="chat")
host = platform.node()
history.append({
"role": "assistant",
"content": response_text,
"backend": actual_backend,
"backend_label": backend_label,
"host": host,
})
save_session(session_id, history) save_session(session_id, history)
if not req.off_record: if not req.off_record:
log_turn(session_id, req.message, response_text) log_turn(session_id, req.message, response_text, backend_label, host)
# fallback_used only makes sense for explicit backend selections. # fallback_used only makes sense for explicit backend selections.
# In auto mode (req.model is None), just report what responded. # In auto mode (req.model is None), just report what responded.
@@ -121,7 +130,8 @@ 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, role="chat"), "backend_label": backend_label,
"host": host,
"fallback_used": fallback_used, "fallback_used": fallback_used,
} }
yield f"data: {json.dumps(payload)}\n\n" yield f"data: {json.dumps(payload)}\n\n"

View File

@@ -3,7 +3,13 @@ from config import settings
from persona import persona_path from persona import persona_path
def log_turn(session_id: str, user_msg: str, assistant_msg: str) -> None: def log_turn(
session_id: str,
user_msg: str,
assistant_msg: str,
backend_label: str = "",
host: str = "",
) -> None:
today = datetime.now().strftime("%Y-%m-%d") today = datetime.now().strftime("%Y-%m-%d")
sessions_dir = persona_path() / "sessions" sessions_dir = persona_path() / "sessions"
sessions_dir.mkdir(exist_ok=True) sessions_dir.mkdir(exist_ok=True)
@@ -12,11 +18,14 @@ def log_turn(session_id: str, user_msg: str, assistant_msg: str) -> None:
timestamp = datetime.now().strftime("%H:%M") timestamp = datetime.now().strftime("%H:%M")
is_new = not log_file.exists() is_new = not log_file.exists()
meta_parts = [p for p in [backend_label, host] if p]
meta = f" · {' / '.join(meta_parts)}" if meta_parts else ""
with open(log_file, "a") as f: with open(log_file, "a") as f:
if is_new: if is_new:
f.write(f"# Session Log — {today}\n") f.write(f"# Session Log — {today}\n")
f.write( f.write(
f"\n### [{timestamp}] `{session_id}`\n" f"\n### [{timestamp}] `{session_id}`{meta}\n"
f"**{settings.user_name}:** {user_msg}\n\n" f"**{settings.user_name}:** {user_msg}\n\n"
f"**{settings.agent_name}:** {assistant_msg}\n" f"**{settings.agent_name}:** {assistant_msg}\n"
) )

View File

@@ -566,6 +566,13 @@
currentHistory.push({ role, content: msg.content }); currentHistory.push({ role, content: msg.content });
const msgDiv = addMessage(role, msg.content); const msgDiv = addMessage(role, msg.content);
attachHistoryControls(msgDiv, i); attachHistoryControls(msgDiv, i);
if (role === 'assistant' && (msg.backend_label || msg.backend)) {
const modelTag = document.createElement('div');
modelTag.className = 'model-tag';
const label = msg.backend_label || msg.backend;
modelTag.textContent = msg.host ? `${label} · ${msg.host}` : label;
msgDiv.appendChild(modelTag);
}
} }
if (!silent) addMessage('system', `Resumed session ${id}`); if (!silent) addMessage('system', `Resumed session ${id}`);
@@ -979,9 +986,10 @@
const modelTag = document.createElement('div'); const modelTag = document.createElement('div');
modelTag.className = 'model-tag' + (data.fallback_used ? ' fallback' : ''); modelTag.className = 'model-tag' + (data.fallback_used ? ' fallback' : '');
const label = data.backend_label || data.backend || ''; const label = data.backend_label || data.backend || '';
const hostSuffix = data.host ? ` · ${data.host}` : '';
modelTag.textContent = data.fallback_used modelTag.textContent = data.fallback_used
? `⚡ fallback → ${label}` ? `⚡ fallback → ${label}${hostSuffix}`
: label; : `${label}${hostSuffix}`;
thinkingDiv.appendChild(modelTag); thinkingDiv.appendChild(modelTag);
} else if (data.type === 'error') { } else if (data.type === 'error') {
throw new Error(data.message); throw new Error(data.message);