From 3b3456600aa4029643b935b53eecf33838ce92b5 Mon Sep 17 00:00:00 2001 From: Scott Idem Date: Wed, 8 Apr 2026 22:16:48 -0400 Subject: [PATCH] feat: store and display backend + host metadata on chat messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- cortex/llm_client.py | 3 ++- cortex/routers/chat.py | 16 +++++++++++++--- cortex/session_logger.py | 13 +++++++++++-- cortex/static/app.js | 12 ++++++++++-- 4 files changed, 36 insertions(+), 8 deletions(-) diff --git a/cortex/llm_client.py b/cortex/llm_client.py index cc583a3..a67f9df 100644 --- a/cortex/llm_client.py +++ b/cortex/llm_client.py @@ -183,7 +183,8 @@ async def _local(system_prompt: str, messages: list[dict], model_cfg: dict | Non msgs: list[dict] = [] if 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 headers: dict[str, str] = {} diff --git a/cortex/routers/chat.py b/cortex/routers/chat.py index eb1608e..6c14173 100644 --- a/cortex/routers/chat.py +++ b/cortex/routers/chat.py @@ -1,5 +1,6 @@ import asyncio import json +import platform import jwt from fastapi import APIRouter, HTTPException, Query, Request from fastapi.responses import StreamingResponse @@ -108,10 +109,18 @@ async def _stream_chat(req: ChatRequest): try: 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) 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. # In auto mode (req.model is None), just report what responded. @@ -121,7 +130,8 @@ async def _stream_chat(req: ChatRequest): "response": response_text, "session_id": session_id, "backend": actual_backend, - "backend_label": _backend_label(actual_backend, user, role="chat"), + "backend_label": backend_label, + "host": host, "fallback_used": fallback_used, } yield f"data: {json.dumps(payload)}\n\n" diff --git a/cortex/session_logger.py b/cortex/session_logger.py index 0882cac..b258de2 100644 --- a/cortex/session_logger.py +++ b/cortex/session_logger.py @@ -3,7 +3,13 @@ from config import settings 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") sessions_dir = persona_path() / "sessions" 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") 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: if is_new: f.write(f"# Session Log — {today}\n") 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.agent_name}:** {assistant_msg}\n" ) diff --git a/cortex/static/app.js b/cortex/static/app.js index 45cbc27..939337b 100644 --- a/cortex/static/app.js +++ b/cortex/static/app.js @@ -566,6 +566,13 @@ currentHistory.push({ role, content: msg.content }); const msgDiv = addMessage(role, msg.content); 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}`); @@ -979,9 +986,10 @@ const modelTag = document.createElement('div'); modelTag.className = 'model-tag' + (data.fallback_used ? ' fallback' : ''); const label = data.backend_label || data.backend || ''; + const hostSuffix = data.host ? ` · ${data.host}` : ''; modelTag.textContent = data.fallback_used - ? `⚡ fallback → ${label}` - : label; + ? `⚡ fallback → ${label}${hostSuffix}` + : `${label}${hostSuffix}`; thinkingDiv.appendChild(modelTag); } else if (data.type === 'error') { throw new Error(data.message);