Releases: bashrusakh/llm-testbench
Release list
v0.5.0
v0.5.0 (2026-06-29)
Major UI/UX overhaul — four new layout variants (V1–V4), six design channels,
SQL compare filter, accessibility and contrast fixes across all themes.
Added
-
Four layout variants (V1–V4) — distinct UI shells for the classic sidebar
experience:- V1 Grid Hero — dashboard-style grid with hero metrics card
- V2 Dock — bottom-docked settings panel with slideout, SVG icon buttons
- V3 Tabs — top-tabbed setup (Endpoints / Models / Settings) with full
keyboard-accessiblerole="tab"ARIA pattern - V4 Overlay — full-screen overlay settings with
label[role="button"]
keyboard support
-
Six design channels (D1–D6) — complete colour-system variants applied
per-layout viahtml[data-design="vX"]+html[data-channel="dY"]:- D1 Default (phosphor accent), D2 Instrument (teal/copper), D3 Monitor
(IBM Plex), D4 Startup (Space Grotesk), D5 Arctic Light (light theme,
WCAG AA), D6 Terminal (monospace amber)
- D1 Default (phosphor accent), D2 Instrument (teal/copper), D3 Monitor
-
Channel selector in sidebar — oscilloscope-style channel picker (D1–D6)
replaces the old 3-way theme toggle; choice persists inlocalStorage. -
SQL Compare model filter — universal text filter anchored over the
Model/Run column in the comparison heatmap bar; live visibility counter,
MutationObserver-guarded against microtask loops; per-channel focus rings
and accent borders. -
Alternative visual themes per V-layout — each V1–V4 layout has a second
standalone CSS theme loaded disabled by default:- V1 Grid Hero ↔ Instrument (Fira Code / Fira Sans)
- V2 Dock ↔ Monitor (IBM Plex Sans / JetBrains Mono)
- V3 Tabs ↔ Startup (Space Grotesk / DM Sans)
- V4 Overlay ↔ Terminal (Share Tech Mono / Fira Code)
Theme switcher button in each V-layout nav persists vialocalStorage.
-
SQL heatmap channel polish — full D5 Arctic Light override block
(sticky headers, hover states, compare-head, result chips, pass/fail cells
on white background); per-channel tinted hover + compare-head rows for D1–D4
and D6; D5 compare-bar visibility fix.
Fixed
-
V2 Dock slideout CSP violation —
data-dock-actionattributes replace
inlineonclickhandlers; slideout toggle/open/close bound inapp.js. -
V3 Tabs accessibility —
label[role="tab"]now syncsaria-selected
with radio:checkedstate for screen-reader tab announcements. -
V4 Overlay keyboard —
label[role="button"]responds to Enter/Space;
mobile.grid-viewportinset restored (0 0 60px 0). -
CSS deprecated
word-break: break-wordreplaced withoverflow-wrap
across v1-instrument, v2-dock, v2-monitor, v3-startup, v3-tabs, sql-grid. -
.sr-onlyrules gainclip-path: inset(50%)alongside legacy
clip: rect()for modern browser support (v1-instrument, v3-startup). -
WCAG AA contrast —
--text-dimraised inv4-terminal.cssand
c1-clean-lab.css(#767676 / #8A8A8A) to meet 4.5:1 on their respective
backgrounds. -
D5 compare bar —
sql-compare-barbackground made visible on the light
Arctic Light surface.
Removed
- A1/A2/A3 and index-themes layout experiments — dead code, never linked
from production, no nav switcher, no routes. Deleted 7 files
(index-a1/a2/a3.html, index-themes.html, variants/a1/a2/a3.css), removed
A2 tab-switching fromapp.js, cleaned upvariants/index.html.
Tests
- Test suite: 217 passed (up from 213).
Verification
python -m pytest→ 217 passedcreate_app()boots and registers routes- All 6 layout/channel combinations load without CSS/JS errors
v0.4.0
v3 Phosphor default design, oscilloscope-style channel selector, Think column
in SQL matrices, and Gemma thinking-mode tool-call fixes.
Added
- v3 Phosphor design — new default design with phosphor accent palette,
replacing the previous default. Switchable via a 3-way theme toggle in the
sidebar. (#54) - Think column in SQL matrices — SQL run comparison heatmap and model
grouping now include a Think column showing thinking-mode status per
question, enabling quick scan of which questions used reasoning. (#56) - Oscilloscope-style channel selector — the design toggle is replaced with
an oscilloscope-style channel selector for switching between v1/v2/v3
designs.
Fixed
- Model header row background — solid dark background across the entire
model header row in all themes, replacing broken colspan-based layouts. - Model header hover squares — hover state indicators work correctly in
both v2 and v3 themes. - SQL heatmap category border — category border no longer overlaps result
cells in v3 design. - Sidebar sticky header overlap — sticky header overlap on scroll fixed in
v2/v3 themes. - Compare runs — unanswered questions are no longer treated as errors in
the comparison view. - Gemma thinking-mode tool calls — parse
<|call:>and bare
run_sql_queryformats from Gemma models in thinking mode, preventing
TypeError on non-string SQL args. (#55) - Think column comparison grouping — fixed comparison grouping for
thinking_mode across SQL matrices. (#56) - Text-fallback SQL success path — missing assistant message added to
conversation on text-fallback SQL success. (#52) - Duplicate SQL detection — TOOL RESULT appended to conversation when
duplicate SQL is detected.
Tests
- Test suite: 217 passed (up from 213).
v0.3.0
Design toggle, SQL run comparison heatmap, Gemma-style tool-call parsing, and
text-fallback SQL retry improvements.
Added
- Design toggle — ✦ button in the sidebar header switches between the default
dark-blue theme and a new instrument-palette theme (teal accent, copper secondary,
Sora for headings, JetBrains Mono for data). Preference persisted inlocalStorage. - Sparkline throughput bars — miniature animated bar charts in the speed results
table showing per-run decode throughput, using teal/copper/red gradient. - SQL run comparison heatmap — visual heatmap table comparing SQL accuracy across
runs, with colour-coded cells for fast scanning of pass/fail per question. (#46) - Model grouping in SQL run comparison heatmap — heatmap rows are grouped by model
name for easier cross-run comparison. (#48)
Changed
- CSS architecture — all v2 design overrides live in a single
html[data-design="v2"]block; the default v1 design is untouched.
Fixed
- SQL leading comments break text-only fallback — leading
--comment lines are
now stripped before the SQL heuristic check. (#45) - SQL execution errors lost in text-fallback branch — widened to
(duckdb.Error, ValueError, TypeError); error is reported back as a tool result. (#47) stop_reasonlabel clarity — retry and fallback labels now distinguish the two
paths. (#47)- Gemma-style
<tool_call>blocks not parsed — parser now recognizes Gemma XML
format alongside existing pipe-prefixed format. (#50) - Last TOOL RESULT missing from conversation — appended before the retry-limit
check so the conversation always includes the terminal error. (#50) - Think-mode wasted retry budget —
MAX_NO_TOOL_CALL_RETRIESreduced from 3 to 1
whenthinking_mode="on". (#50)
Tests
- 26 new tests; test suite 213 passed (up from 187).
v0.2.3
chore(v0.2.3): release — run comment, HTTP pooling, SQL watchdog, sco…
v0.2.2
Major refactor — code-review cleanup pass closes an SSRF gap in the SQL path, stops the backend from overriding server-side sampling settings, surfaces the tool-call budget and stop reason in the SQL detail card, adds a no-hardcode version resolver, and removes a pile of dead/misleading code uncovered by review.
⚠️ MAJOR — Heads up before you re-run anythingSampling parameters (
temperature/top_p/presence_penalty/frequency_penalty) are no longer sent in any LLM request.Previously the backend pushed these values into every OpenAI-compatible payload. On OpenAI-compatible servers the client-sent value replaces the model's registered preset, so whatever you configured in LM Studio / llama.cpp / Ollama was being silently discarded by us. From v0.2.2 the server uses its own preset — that's the source of truth now.
Why the old behaviour was actively harmful for SQL benchmarks:
the SQL tool-calling path was pinned totemperature: 0.1(hard-coded incall_llm_singleandcall_llm_tool_calling). That value runs the model in near-deterministic mode. Combined with the tool-calling retry loop (run_question_tool_calling— up tomax_retries=3SQL retries after a DuckDB error, plus theMAX_TOOL_CALLS=10overall ceiling) the result was:
- Model generates wrong SQL on attempt 1.
- DuckDB returns a
BinderException/ParserException.- We feed the error back and ask for a fix.
- At
temperature=0.1the model produces the same wrong SQL again — sampling is too tight to escape the local optimum.- Repeat until
MAX_TOOL_CALLSis hit. Question fails withstop_reason=tool_call_limiteven though the model had room to try something different at any other temperature.What you should do after upgrading:
- Re-run anything you compared between models — earlier "model X can't recover from BinderException" results were partly our fault, not the model's.
- Set your sampling preset on the LLM server side (LM Studio model settings, Ollama
Modelfile, llama.cpp launch flags). The backend no longer pushes back.- If you want strict determinism for reproducibility runs, set
temperature=0in the server preset — but understand the SQL retry loop will degrade exactly the way described above. For benchmarking,temperature=0.7or whatever the model recommends is usually saner.
Added
GET /api/version+ sidebar version tag — backend resolves the running app's version on first call and caches the result; the UI fetches it at startup and renders it next to the "LLM Testbench" title with a tooltip showing where the string came from..github/workflows/release.yml— on tag push (v*) it writes the release tag into./VERSION, zips the working tree, and attaches the archive to the GitHub release.- SQL detail card — Tool-call budget — the meta strip now shows
Tool calls: 5 / 10, highlighting the chip in amber at ≥80% ofMAX_TOOL_CALLSand red at 100%. - History table —
QuestionsandThinkingcolumns — two new columns so you can scan saved runs and tell at a glance which SQL questions were exercised and which thinking variant ran. - SQL detail card — Stop reason — the backend
stop_reasonis surfaced as a coloured chip with a plain-English label. Hidden on the happy path (results_ok+success=true). - CSS —
--warnand--dangerdesign tokens added to:root.
Security
- SSRF guard now covers the SQL path —
call_llm_singleandcall_llm_tool_callingwere issuing HTTP requests to user-supplied endpoints without the_validate_endpoint_urlcheck that speed-mode runs already enforce.
Fixed
⚠️ Stopped overriding server-side sampling parameters — see the heads-up above.timeoutMsdefault was 12000 seconds (~3.3 hours) — fixed to120(= 120 s real timeout).- SQL execution narrowed to
except duckdb.Errorcould still crash the job — widened to(duckdb.Error, ValueError, TypeError). - Speed benchmark lost all in-progress results on crash —
run_sequentialandrun_parallelnow flush after every appended row. fallback_completion_tokensreported chunk count, not token count — replaced with explicitNone(UI showsn/a).- Mixed
latency_msandlatency_srows were averaged without conversion — normalised seconds → ms before averaging. - "Stop failed" misreported as "Start failed" — copy-paste error fixed.
- Speed table
colspan="12"against an 11-column table — fixed to11. - Dead
len(models) < 1guard inBenchmarkRequest.from_dict— removed.
Changed
- CI actions bumped to Node 24 runtimes — clears the
Node.js 20 actions are deprecatedwarning. - Test suite: 150 tests passing (142 baseline + 8 new in
tests/test_version.py).
v0.2.1
Fixed
_finalize_tool_runcrash on invalid LLM-generated SQL —duckdb.Error(e.g.BinderExceptionfor missing GROUP BY) was not caught, crashing the entire benchmark job instead of recording a failed result. (#32)
LLM Testbench v0.2.0
v0.2.0 (2026-06-10)
Major refactor — backend split into focused modules, frontend extracted to separate CSS/JS files, SSRF security hardening, performance optimizations, and numerous UI/UX fixes across speed and SQL views.
Security
- SSRF guard for provider endpoints —
_validate_endpoint_urlresolves hostnames viasocket.getaddrinfoand rejects targets whose resolved IPs fall into multicast/reserved/unspecified ranges. Loopback and RFC 1918 ranges are permitted for local use. Wired into_benchmark_openai,_benchmark_ollama, and_probe_provider.
Added
tests/test_speed_unit.py— 26 regression tests for aggregates, SSRF guard, OpenAI/Ollama benchmark paths, JSON I/O, phase atomicity, aggregate cache, and save-task draining.
Changed
- Server split into focused modules —
python/server.pyreduced from 3035 to 168 lines. Core logic extracted into:python/models.py— dataclasses and registries (EndpointCandidate, BenchmarkRequest, RunState, JobState, etc.)python/aggregates.py— per-model speed rolluppython/json_io.py— JSON load/read with path-tagged errorspython/ssrf.py— endpoint URL private-IP guardpython/speed_row.py— uniform row builder for success/failure/stoppedpython/persistence.py— job save/load/export (538 lines)python/job_runner.py— benchmark orchestration (825 lines)python/benchmark_server.py— HTTP routes and delegate methods (1012 lines)
- Frontend split into
static/style.cssandstatic/app.js—index.htmlshrinks from 3971 to 321 lines. CSS and JS are loaded via<link>and<script>tags. - Redundant OPTIONS catch-all route removed — aiohttp CORS middleware already handles preflight.
- Persisted
aggregated_speed— on-disk job records now include pre-computed aggregates so history opens in aggregated mode immediately. set_phaseis now atomic — five phase fields written as a singleRunStatesnapshot underasyncio.Lock.- Per-job save lock + tracked save tasks — serialises snapshot+write per job; parallel jobs don't race.
_compute_speed_aggregatesmemoised perJobState— keyed on(len(results), last_result_timestamp). Cache turns O(n) poll walk into O(1)._build_speed_rowhelper — unified row-dict builder, eliminated ~80 lines of duplication.- Silent
except Exceptionblocks narrowed — all backend modules now catch specific exception types. - Per-row DOM diff in speed results —
Map<rowKey, {row, version}>cache eliminates DOM mutations on unchanged poll ticks. - Frontend helpers —
setStatusBoth,emptyState,getActiveJobId,formattersobject,OUTCOME_METAmap centralise repeated UI patterns. - Accessibility —
aria-live,role="status",aria-busy,aria-labeladded to status and results elements. - Poll timer leak fixed —
stopPolling()clears pendingsetTimeout; registered onpagehide+beforeunload. - Inline
onclickmigrated todata-actiondelegation — works under strict CSP. - Removed dead code —
BenchmarkServer._read_jsonl, unused frontend helpers, unused DOM ids, unused imports, empty__init__.py.
Fixed
- Speed
decode_tps~2× lower for reasoning models — TTFT anchor now triggers on firstdelta.reasoning_contentordelta.content, matching LM Studio'seval_count / eval_duration. - Ollama
latency_mswas actuallyttft_ms— nowlatency_ms == total_time_msfor both providers. - Bare
json.loads(...)produced errors with no file name — re-raised asValueError("Invalid JSON in {name}: ..."). - Incremental saves could be lost on SIGTERM —
drain_pending_saves()+BenchmarkServer.shutdown()await every pending save before exit. - Speed results stay visible during live run — failed-only models now appear in aggregated projection.
- Aggregated/Individual toggle works mid-run —
renderResultsrespectsstate.speedViewModewithout silent fallback. - Live updates not blocked by open history view —
startNextQueuedJobresetsactiveHistoryJobIdon new benchmark. - Speed results no longer show "—" for missing usage block — falls back to chunk-counting when backend omits
usage. - HTTP 200 with SSE
errorbody is now a hard failure — raisesRuntimeErrorinstead of producing phantom passes. - Parallel mode no longer drops results on cancellation —
CancelledErrorconverted tostopped_result. - Speed results columns aligned —
table-layout: fixed,tabular-nums, explicit per-column widths across raw, aggregated, and detail tables. - History Open race condition fixed — debounce flag, generation counter, deduplication guard.
- Speed view toggle preserves global state —
syncSpeedToggleButton()is single source of truth. - History Close clears speed table — both
resultsBodyandspeedResultsContaineremptied. - History Open updates status — shows opened job ID and status.
- Raw/aggregated table widths aligned — toggling no longer causes visible width jump.
- Aggregated TTFT in seconds — header and values consistently use seconds.
- Timeout inputs in seconds — labels, defaults, and JS payloads use seconds.
- Number inputs right-aligned — consistent numeric presentation.
- Sidebar visual overhaul — modernised spacing, typography, and interactive states.
- CSS specificity of aggregated/detail columns raised — scoped under
#speedResultsContainer > table.speed-aggregated-tableand#speedResultsContainer .run-detail-tableto beat raw-view rules. - Expand-toggle button padding fixed —
padding: 0prevents inheriting6px 12pxfrom genericbuttonrule.
LLM Testbench v0.1.1
What's Changed
- Add repository triage automation setup by @bashrusakh in #1
- Fix/speed benchmark max tokens by @bashrusakh in #2
- fix: add gitattributes for line endings by @bashrusakh in #3
- Feat/speed UI aggregated by @bashrusakh in #4
- Feat/speed UI aggregated by @bashrusakh in #5
- fix(speed): decode t/s for reasoning models, aggregated view, column … by @bashrusakh in #6
New Contributors
- @bashrusakh made their first contribution in #1
Full Changelog: v0.1.0...v0.1.1
LLM Testbench v0.1.0
Removed
- Removed BFCL benchmark support from the backend, UI, fixtures, tests, README, and roadmap.
- Removed BFCL fixture data and the dedicated BFCL adapter module.
Fixed
- Model selection controls are now locked while a benchmark job is queued, running, or stopping.
- SQL result rendering no longer shows empty tool-call analysis blocks for empty
results_okconfirmation calls. - SQL model quantization badges now recognize Unsloth Dynamic GGUF labels such as
UD_IQ1_Sinstead of falling back to an unknown format.
Changed
- Startable benchmark modules are now limited to Speed and SQL Accuracy.
- Fixture validation now covers SQL, Coding Micro, JSON Schema, and Prompt Replay only.
- The README and roadmap now describe the project as a fast, lightweight local benchmark workbench.
- The test badge and development instructions now report 116 passing tests.
Verification
- Test suite:
116 passed.