8.0 KiB
Compare
Last updated: dev@c74f475 | 2026-10-15
Scope
This spec covers model A/B comparison behavior in:
- canonical
routes/compare/compare_routes.py, withroutes/compare_routes.pyas a compatibility shim; routes/session_routes.py;routes/chat_routes.pyandroutes/chat_helpers.py;routes/model_routes.py;- canonical
routes/search/search_routes.py, withroutes/search_routes.pyas a compatibility shim; core/database.pymodelComparison;src/llm_core.pyandsrc/endpoint_resolver.py;- frontend modules under
static/js/compare/; static/js/chat.js,static/js/sessions.js,static/js/models.js, andstatic/js/slashCommands.js;tests/test_compare_*and focused blind-compare redaction tests.
Runtime Behavior
The active text compare UI creates ordinary [CMP] sessions through /api/session, then streams each pane through /api/chat_stream with compare_mode=true. Search compare is a separate branch: it can query /api/search/query directly and its synthesis sessions use ordinary chat streaming without compare_mode=true. static/js/compare/index.js owns compare orchestration, session creation, execution order, search-mode branching, and export actions. static/js/compare/panes.js owns pane add/remove/swap/reroll lifecycle. static/js/compare/stream.js owns pane streaming and event rendering.
routes/compare/compare_routes.py owns the /api/compare HTTP surface for alternate/legacy start/vote/history/delete behavior and the active /api/compare/record vote-summary endpoint. The top-level module is a compatibility alias. Legacy /api/compare/start uses neutral helper-session names and withholds model identities/mapping from the start response while blind mode is active. It does not own provider-specific payload behavior.
Current call sites include:
/api/sessioncompare session creation and cleanup in compare frontend modules;/api/chat_streampane execution through chat routes and detached stream infrastructure, streamed directly into panes so upstream generation stops promptly when panes are stopped;/api/modelsand probe routes for model/endpoint selection;- search-provider compare mode through
routes/search/search_routes.py; /api/compare/recordas a fire-and-forget backend vote summary, while active scoreboard state is localStorage-backed.
Comparison rows currently persist vote/history metadata: prompt, first model identifiers, winner, blind flag, optional N-model JSON in blind_mapping, vote timestamp, and owner. Response and metric columns exist in the schema but are not populated by the active compare UI flow. Compare history must be owner-scoped.
Frontend compare behavior is split by responsibility:
state.jsowns local compare state;selector.js,models.js, andprobe.jsown endpoint/model selection and probe UI;panes.jsandstream.jsown paired response rendering;vote.jsandscoreboard.jsown voting and history display.
Compare panes can receive ask_user or tool-approval controls from the shared chat stream. static/js/compare/stream.js renders shared choice controls inside the originating pane history, pauses pane completion/autograding while a choice is pending, and resumes only while the pane session still matches. Continuation waits for the originating controller to release the pane, restores the card after a ten-second timeout, and refuses to send a stale choice into a replacement controller; compare orchestration keeps its busy state until those continuations settle.
Mobile compare uses selectable pane tabs and one visible active pane; desktop retains the selected multi-column grid. Pane add/remove/shuffle paths keep active-tab and session/settings indices aligned.
Ownership Boundaries
Compare owns paired evaluation flow and pane state. Chat routes own the actual stream execution path for compare panes. LLM provider code owns model-call mechanics. Session/model routes own endpoint-id resolution, owner-filtered endpoint/model visibility, header copying, and deleted-endpoint failures.
compare_mode in chat strips compare-breaking tools, disables document tools for [CMP] sessions, skips some research clarification, and suppresses memory, skill, and webhook side effects after pane responses.
Compare frontend code is part of the app DOM security surface. Current stream/search rendering sanitizes probe labels and tool labels, constrains search-result links to HTTP(S), uses safe generated-image display sources, and opens compare export/image popups with opener isolation.
Policy Notes
- Current blind compare is UI/API masking until vote/reveal, not a full confidentiality boundary.
[CMP]session names and session-list model fields are redacted for helper sessions, and legacy/api/compare/startwithholds model identity/mapping while blind. Client-side selected model state and privileged/local inspection can still expose identity. - Compare endpoint lists and secondary endpoint lookups use owner filtering so users see and resolve only shared or owned endpoints.
- Non-admin compare session creation must use registered owner-visible endpoints; compare must not allow arbitrary raw endpoint URLs to bypass session-route endpoint policy.
- Prefetched search, URL, RAG, and research context entering compare panes must use the untrusted-context wrapper.
- Compare panes use chat's foreground routing contract: selected routes are strict unless that owner explicitly enabled ordered foreground fallbacks. Verify each pane still reaches its intended route and that any opt-in route transition or error is visible.
Degraded And Compatibility Behavior
- Missing/offline endpoints are surfaced by model/session routes; chat can clear orphaned endpoint references and recover empty models when possible.
- Compare streams inherit chat's opt-in, eligible-pre-output-only foreground fallback and provider-normalized SSE events, but compare frontend handling for errors and model/endpoint route transitions is thinner than chat's stream path.
- Shared legacy
ModelEndpoint.owner == NULLrows remain visible through owner filters. LegacyComparison.owner == NULLrows are not treated as shared for authenticated vote/delete/history flows. /api/compare/startand/{comp_id}/voteremain implemented but are not the active frontend path.
Per-Pane Generation Controls
static/js/compare/panes.js owns per-pane inference controls and saves thinking_mode, temperature_override, and max_tokens_override to /api/session/{id}/generation-settings. The backend session/provider path owns the actual generation semantics. Pane creation initializes overrides, removal and shuffle preserve index alignment, and mandatory-thinking model controls are disabled where identified. stream.js renders response statistics including token/latency/context/cost metrics and the thinking-off indicator; compare header statistics open a compact details popup.
Shared Celebration Renderer
static/js/compare/vote.js imports and reexports spawnConfetti from static/js/shared/ui/confetti.js, published from frontend/src/shared/ui/confetti.js. Notes imports the shared renderer directly; voting no longer owns its implementation. Vote behavior and callers retain the compatibility export.
Current Gaps
- Blind mode is not a confidentiality boundary; client/local state can still expose model identity before vote.
/api/compare/startaccepts raw endpoint URLs and can diverge from/api/sessionendpoint-owner/raw-endpoint policy.src/agent_loop.pyadvertises stale compare app API endpoints.- Compare streaming and chat streaming are separate frontend paths but share model/provider infrastructure; regressions can happen when provider event shape changes.
- Compare frontend needs explicit fallback/error event handling parity with chat streaming.
- Compare tests cover endpoint owner helper behavior, blind compare redaction, ask-user/tool-approval routing, and portable JS helpers, but not full active
/api/sessionpane creation, frontend pane lifecycle, or complete SSE fallback/error handling.