fix(compare): stop blind mode leaking model identities via session names (#1318)

Blind Compare anonymized the pane headers, but each pane still created a helper chat session named "[CMP] <real-model>" and GET /api/sessions returned the session's model field. So the sidebar and the session-list API let a user map "Model A" back to its real model before voting, defeating the blind test.

- Frontend (static/js/compare/index.js, panes.js): in blind mode, name helper sessions by their neutral slot ("[CMP] Model A") instead of the model, matching the existing blind pane labels.
- Backend GET /api/sessions (routes/session_routes.py): blank the model field for [CMP]-prefixed helper sessions via a new _public_model helper.
- Backend /api/compare/start (routes/compare_routes.py): name blind sessions by slot and withhold model_left/model_right/mapping from the blind response (revealed at /vote).
- Tests: tests/test_blind_compare_redaction.py.

Fixes #1285.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rudy Wolf
2026-06-04 06:39:01 +03:00
committed by GitHub
parent 3d8c364689
commit 1c43daa564
5 changed files with 153 additions and 22 deletions

View File

@@ -210,7 +210,9 @@ async function _buildCompareUI() {
for (let i = 0; i < n; i++) {
const m = state._selectedModels[i];
const fd = new FormData();
fd.append('name', '[CMP] ' + modelShorts[i]);
// Blind mode: name the session by its neutral slot so the sidebar /
// GET /api/sessions can't de-anonymize the comparison (issue #1285).
fd.append('name', '[CMP] ' + (state._blindMode ? 'Model ' + _slotChar(i) : modelShorts[i]));
fd.append('endpoint_url', m.endpoint || '');
fd.append('model', m.model || '');
if (m.endpointId) {

View File

@@ -382,7 +382,8 @@ async function _createAndAppendPane(m) {
// Create session
const fd = new FormData();
fd.append('name', '[CMP] ' + m.name);
// Blind mode: neutral slot name only — never leak the model (issue #1285).
fd.append('name', '[CMP] ' + (state._blindMode ? 'Model ' + _slotChar(i) : m.name));
fd.append('endpoint_url', m.url || '');
fd.append('model', m.id || '');
if (m.endpointId) {
@@ -584,7 +585,8 @@ function _showModelSwapDropdown(paneIdx, titleBtn) {
fetch(`${state.API_BASE}/api/session/${oldSid}`, { method: 'DELETE' }).catch(() => {});
}
const fd = new FormData();
fd.append('name', '[CMP] ' + m.name);
// Blind mode: neutral slot name only — never leak the model (issue #1285).
fd.append('name', '[CMP] ' + (state._blindMode ? 'Model ' + _slotChar(paneIdx) : m.name));
fd.append('endpoint_url', m.url || '');
fd.append('model', m.id || '');
if (m.endpointId) {