U of I × MindRouter App Gallery

AI Workload Estimator

Research Built with OpenCode v1.18.21 Critic status: shipped

AI Workload Estimator

Plan an LLM-assisted research project on MindRouter

Planning to have an AI read 40,000 survey comments or 2 million abstracts? Estimate the tokens, wall-clock time and quota you will need on MindRouter before you start — and pick the right model.

AI Workload Estimator running on a desktop screen, plan an llm-assisted research project on mindrouter Desktop · click to enlarge
AI Workload Estimator running on a phone, plan an llm-assisted research project on mindrouter
Phone · click to enlarge

What it is ✎ blurb written by the AI critic

Plan a batch AI job on MindRouter before you run it. Describe your documents (or paste a sample), pick a task and a model from a snapshot of MindRouter's model catalog, and see estimated tokens, run time, quota use and how each item gets chunked to fit the model's context window. It also compares three models side by side and writes a ready-to-run Python batching script with your settings.

Try this

  1. Click the '500 interview transcripts (20 pages) → summarize' preset, then choose aya-expanse:8b as the model to see each transcript split into 2 chunks and compared with larger-context models.
  2. Paste a few real survey responses or abstracts into the sample box, separated by blank lines, and watch the token counts, fit check and chunk diagram update.
  3. Type a requests-per-minute limit in the Quota panel; if the job exceeds it, click 'apply' to set the highest concurrency that fits, then drag the Output length slider to see the time change.

Highlights ✎ written by the AI critic

  • Model picker built from a 2026-09-13 snapshot of MindRouter's 73-entry model catalog, with context-window fit checks and chunk counts
  • Live token, wall-clock and quota estimates with a worker timeline and a ±50% sensitivity (tornado) chart
  • Chunking visualizer that draws the context window, the item split into chunks, and which window sizes fit
  • Side-by-side comparison of any three models that flags models of the wrong type for the task
  • Copyable Python script (OpenAI-compatible client, retries, chunking, batched embeddings) generated from your settings

Who it's for

Researchers planning to process documents, transcripts, survey responses or code with AI

Features

  • Corpus description form: number of items, average length (words, pages or characters), with a paste-sample box that measures a real sample's tokens (approximate tokenizer in JS)
  • Task templates (summarize, classify, extract fields to JSON, translate, embed for search) that set typical output lengths
  • Model picker from the catalog snapshot with context-window fit checks (does one item fit? how many chunks?)
  • Results: total input/output tokens, estimated time at chosen concurrency, and a Gantt-like timeline; sensitivity sliders
  • Comparison table across 3 models and a chunking visualizer
  • Copyable Python batching snippet that calls MindRouter's OpenAI-compatible endpoint with the chosen settings

Data & sources

  • mindrouter.uidaho.edu — MindRouter model catalog snapshot, GET
  • Throughput, wall-clock time and quota figures: user-editable estimates in the UI, labelled as estimates, not real University of Idaho figures
Full data provenance ✎ written by the AI reviewer

Raw data sources: ai-workload-estimator

Retrieval date for everything in this directory: 2026-09-13. All files are unmodified server responses. Nothing here was filtered, reshaped or aggregated.

FileBytesSHA-256
mindrouter-models.json29,840bcde4378e59597f203b7153cf9932b5cf5b65829f8be36d7c9243997e8542d8e

Total: 29,840 bytes (about 0.03 MB), plus this file.


1. mindrouter-models.json: MindRouter model catalog snapshot

URL: https://mindrouter.uidaho.edu/v1/models Method: GET, no request body. Request headers: Authorization: Bearer <MindRouter API key, redacted and not stored anywhere>, Accept: application/json, User-Agent: UIdaho-gallery-data-prep/1.0 Response: HTTP 200, Content-Type: application/json, Content-Length: 29840, server Date: Sun, 13 Sep 2026 18:02:50 GMT.

Publisher: University of Idaho, Research Computing and Data Services (RCDS), Institute for Interdisciplinary Data Sciences (IIDS). MindRouter is the university's OpenAI-compatible LLM gateway and load balancer.

Access: you need a MindRouter API key. Without one the endpoint returns HTTP 401 with {"detail":"Missing API key. ..."}, so the app should not link readers to the raw endpoint. For a public link, use the MindRouter home page https://mindrouter.uidaho.edu/ (verified 2026-09-13: HTTP 200, page title "MindRouter - Cluster Status").

  • What it contains: the OpenAI-style model list MindRouter served at retrieval time. It covers every text-in/text-out model (chat, completion, multimodal-vision, embedding, reranking, OCR) on backends that were healthy at that moment. Server-side exclusions, per the MindRouter source (backend/app/api/models_api.py): image-generation, video and speech models (these have their own endpoints: /v1/images/models, /videos/models, /v1/audio/voices)

  • data-loss-prevention scanner engines

  • any model whose only backends were unhealthy at retrieval time

This is a point-in-time snapshot. The live catalog changes as backends go up or down and models are added or retired.

Record count: 73 entries in data[]. That is 63 real model ids plus 10 aliases (is_alias: true). All id values are unique.

Date range: created runs from 1772931628 (2026-03-08T01:00:28Z) to 1787657745 (2026-08-25T11:35:45Z). These are not model release dates (see the field notes below).

Structure and JSON paths
{ "object": "list",
  "data": [ { model }, ... ] }      // sorted by id ascending, plain code-point order
                                    // (uppercase before lowercase: "Nemotron-..." and "Qwen/..." come before "aya...")

Every entry has all 13 keys below, and none are ever missing. Nullable fields appear as JSON null; no other missing-value codes are used.

PathTypeMeaning
data[].idstringThe model name to pass as "model" in /v1/chat/completions, /v1/embeddings, etc. Two naming styles appear. org/name ids (e.g. openai/gpt-oss-120b, qwen/qwen3.6-35b) are served by vLLM. name:tag ids (e.g. qwen2.5:72b, llama3.3:70b) are Ollama tags, where the tag usually encodes the parameter size (:8b, :70b, :8x7b). Plain names (e.g. Nemotron-3-Super-120b, dots.MOCR) and default-* aliases also appear.
data[].objectstringAlways "model".
data[].createdintegerUnix epoch seconds, UTC. The time the model's record was created in MindRouter's registry (first discovery on the cluster). If the record had no timestamp, the server substitutes the request time. Not a release date. Aliases copy their target's value.
data[].owned_bystringAlways "mindrouter". It does not identify the model's developer.
data[].capabilitiesobject of 5 booleansAlways contains exactly multimodal, embeddings, structured_output, thinking, tools. See the caveats below.
data[].capabilities.multimodalboolTrue if any serving backend reports image-input (vision) support. 24 entries are true.
data[].capabilities.embeddingsboolSet by name only: true when the id contains "embed" (case-insensitive). 2 entries are true: Qwen/Qwen3-Embedding-8B and default-embedding. Note that the reranker Qwen/Qwen3-Reranker-8B is false.
data[].capabilities.structured_outputboolHard-coded true for every entry by the server (73/73). It carries no information and should not be treated as a verified per-model capability.
data[].capabilities.thinkingboolTrue if any backend reports reasoning/"thinking" support. 24 entries are true. Reasoning is opt-in per request on MindRouter.
data[].capabilities.toolsboolTrue if any backend reports tool/function-calling support. 52 entries are true.
data[].backendsarray of stringsInternal names of the healthy MindRouter backends serving the model at retrieval time (e.g. "aspen2-gpu3-nemotron3"). Array length ranges from 1 to 5: 11 entries have 1, 37 have 2, 13 have 3, 3 have 4, 9 have 5. The length is the number of serving replicas, a rough hint of capacity, not a throughput figure. Aliases copy their target's list.
data[].context_lengthintegerEffective context window in tokens (prompt plus generated output combined) that MindRouter actually serves. When a model runs on several backends, this is the maximum across them. For Ollama backends it is the configured num_ctx, or else min(architectural maximum, 32768), or else 4096. For vLLM backends it is the served maximum model length. Use this field for "does one item fit / how many chunks" checks. Values seen: 4096 (6 entries), 8192 (7), 16384 (2), 32768 (39), 131072 (5), 262144 (14).
data[].model_max_contextinteger or nullThe model's architectural maximum context in tokens, as reported by Ollama model metadata. It is non-null for 48 entries (all Ollama-served) and null for vLLM-served models and their aliases. This can exceed what MindRouter serves (e.g. leanstral:119b has 1048576 here but serves 32768), so do not use it for fit checks.
data[].parameter_countnullPresent in the schema (a string like "70B" when known), but null for all 73 entries in this snapshot.
data[].quantizationstring or nullWeight quantization reported by the backend: Q4_K_M (34), Q4_0 (12), Q8_0 (1), F16 (1), null (25). The null entries are vLLM-served models and aliases. null does not mean unquantized.
data[].familynullPresent in the schema, but null for all 73 entries in this snapshot.
data[].is_aliasbool or nulltrue for the 10 alias entries, null (not false) for real models.
data[].alias_targetstring or nullFor aliases, the id of the real model the alias routes to. Aliases inherit that model's capabilities, backends, context_length, model_max_context, quantization and created. The 10 aliases in this snapshot: default-agent→qwen/qwen3.8-27b, default-agent-small→qwen/qwen3.6-35b, default-coder→qwen/qwen3.8-27b, default-embedding→Qwen/Qwen3-Embedding-8B, default-llm→openai/gpt-oss-120b, default-llm-large→qwen/qwen3.5-122b, default-llm-small→qwen/qwen3.6-35b, default-ocr→dots.MOCR, default-reranker→Qwen/Qwen3-Reranker-8B, default-vision→qwen/qwen3.6-35b.
Caveats for the app
  • No throughput, latency, tokens-per-second, pricing, quota or rate-limit data is in this file. Every time or throughput figure the app shows (tokens/second per model class, parallel requests, wall-clock time) is an estimate and must be labeled as one in the UI.

  • Capability flags are aggregated across backends and are not uniformly meaningful for non-chat models. The embedding and reranker entries report thinking: true and tools: true, and structured_output is always true. For task-template filtering, embeddings (name-based) is the only reliable "embedding model" signal. Qwen/Qwen3-Reranker-8B / default-reranker is a reranker, not a chat model, and dots.MOCR / default-ocr is an OCR/vision model.

  • Entries with is_alias: true duplicate a real model. De-duplicate them, or label them as aliases, when counting models or building comparison tables.

  • Token counts in context_length refer to each model's own tokenizer. An approximate JavaScript tokenizer gives only an estimate of fit.

License / terms: no open-data license is published. This is service metadata from the University of Idaho's MindRouter service, retrieved with an authorized university API key for use in this university gallery app. Model names and weights belong to their respective developers under their own licenses, which this file does not cover.

Suggested citation: MindRouter model catalog (/v1/models), University of Idaho Research Computing and Data Services; snapshot retrieved 2026-09-13. https://mindrouter.uidaho.edu/

How it was built

Written by OpenCode 1.18.21 (opencode.ai) on qwen/qwen3.8-27b, served by MindRouter, the University of Idaho's on-premises AI platform.

15harness rounds
169.6minutes of harness work (2.8 h)
2,766lines of code
182KB, single-file app

Rounds by kind

build1
feedback7
continue5
critic2

Lines of code per file

body html322
app css505
app js1,843
scripts96

Critic scores ✎ scored by the independent AI critic, 1–10

visual 8 out of 10
usefulness 8 out of 10
correctness 8 out of 10
interactivity 8 out of 10

Independent audits ✎ by the AI critic

Data honesty: checked against raw data Authorship audit: every app-code commit is a harness round

Automated checks

Console no console errors No network 0 external requests Phone layout 0 px overflow Canvases no empty canvases Brand header header present No key leak no key in source

Checked by critic-verify.

Data spot checks ✎ written by the AI critic

  • qwen/qwen3.8-27b: UI 'window 262,144 tok · 5 replicas · vision, thinking, tools' vs raw context_length 262144, 5 backends, multimodal/thinking/tools true - match
  • aya-expanse:8b: UI 'window 8,192 tok · arch max 8,192 · Q4_K_M · 5 replicas · tools' vs raw 8192 / model_max_context 8192 / Q4_K_M / 5 backends / tools true, thinking false - match
  • granite3-dense:8b: UI 'window 4,096 tok · arch max 4,096 · Q4_K_M · 2 replicas' vs raw 4096 / 4096 / Q4_K_M / 2 backends - match
  • Sources section '73 entries (63 models + 10 aliases)' vs raw data[] 73 entries with 10 is_alias:true; embedded models.json cross-checked field by field against raw: 0 mismatches
  • Computed (independent Python): default 40,000 items x 120 words, Summarize, qwen3.8-27b -> 3.41 s/call, 34,100 s = 9.47 h; tokens 6.24M + 6.0M prompt + 2.4M out = 14.64M -> UI '9.5 h · 15M tokens · 40,000 calls', input tile 12M - match
Show all 10 spot checks
  • Computed: 2M abstracts, Embed -> ceil(2,000,000/64)=31,250 requests x (64x234/4000+0.5=4.244 s)/4 = 33,156 s = 9.21 h, 56.6 req/min -> UI '9.2 h · 468M tokens · 31,250 requests', 56.6 req/min - match
  • Computed: 500 transcripts x 20 pages, Summarize -> 7,800 in / 1,170 out, 60.55 s/call, 7,569 s = 2.10 h, 4.56M tokens -> UI '2.1 h · 4.6M tokens · 500 calls' - match
  • Computed: 10,000 code files x 900 words, Extract, qwen2.5-coder:14b (small class) -> 4.525 s/call, 11,312 s = 3.14 h -> UI '3.1 h' - match
  • Computed: RPM limit 60 on default -> 4x60/3.41 = 70.4 req/min, max concurrency floor(60x3.41/60)=3; after 'apply 3' 52.8 req/min -> UI identical - match
  • Computed: Output length slider 2x -> base 16.14 h, output +/-50% = +/-6.67 h -> tornado 'now 16 h 8 m ; Output length -6.7 h ; +6.7 h' - match
Build notes ✎ written by the AI reviewer

The app is shipped and fully built by OpenCode 1.18.21 on MindRouter default-agent. I never edited src/, scripts/, data/embed/ or index.html. My only project writes were FEEDBACK.md, later removed in commit 7b25c84; data/raw was already prepared and committed by the data-prep workflow. The tamper guard never fired.

  • Twelve rounds, 141.6 harness minutes: Build: round 1.

  • Feedback: rounds 2, 3, 5, 7, 8, 9 and 10 (7 of the 8 allowed).

  • Continue re-runs of stuck or spoiled rounds: rounds 4, 6, 11 and 12.

  • The harness repeatedly got stuck or produced nothing: Round 3 looped the same grep more than 169 times for 28 minutes without changing a file. I killed it and re-ran it as round 4, which fixed the items.

  • Round 5 looped 12 identical failed Edit calls ('oldString and newString are identical'). I killed it; it had kept partial changes, and round 6 finished the job.

  • Round 10 read six files, emitted degenerate '9 9 9…' output, made zero edits and exited 0.

  • Round 11 exited after 45 seconds with no edits.

  • Round 12 issued 154 invalid Read calls (missing filePath) with zero edits over 14 minutes, so I killed it.

None of these had network errors. After three consecutive spoiled rounds I stopped and kept the round-9 build, verified byte-identical (index md5 ffcc46d9) and re-verified clean.

  • How feedback worked: I tested with verify_app.py plus my own Playwright scripts. I also ran each generated Python script against a stub openai module, and swept concurrency 1-64 and extreme inputs with timing. The harness acted on almost every finding. Bugs it fixed from that feedback: Phone overflow.

  • Uniform throughput making the comparison meaningless.

  • A NameError-crashing Python script (a JS boolean pasted into Python).

  • Gantt blocks squeezed into a few pixels.

  • NaN SVG errors at 7, 8, 9, 13, 14, 20 and 25 parallel.

  • A chunk-math bug that reserved the whole item's output in every call (e.g. 11,701 chunks and 54 years for translating 30-page documents).

  • An O(calls) loop that froze the tab for 192 s.

Only three cosmetic items from the last feedback round remain unfixed; they are listed under features_missing_or_broken. One parentless Google Chrome process seen during testing was not mine (my scripts use Playwright's bundled Chromium), so I left it alone. No servers or browsers of mine are left running.

Round-by-round timeline

Every harness round, in order. The prompts and FEEDBACK.md were written by the AI reviewer; the app code was written by the harness. Expand a round to see the exact prompt and the FEEDBACK.md the harness received.

    1 build 10.4 min · 6 files changed, 2578 insertions(+), 3 deletions(-) Show prompt

    Prompt given to the harness:

    Read AGENTS.md, SPEC.md and BRAND.md in this directory. Then build the complete app they describe, step by step: first write scripts in scripts/ that turn the raw data in data/raw/ (see data/raw/SOURCES.md — it is a snapshot of MindRouter's /v1/models catalog; read its field notes and caveats carefully) into compact JSON in data/embed/ and run them; then write src/body.html, src/app.css and src/app.js; run python3 build.py --check and fix any error it reports. Implement every required feature in SPEC.md with a polished, modern, responsive University of Idaho design. Throughput assumptions (tokens/second per model class, parallel requests) are not in the data: make them editable in the UI and clearly label them as estimates. Do not stop until python3 build.py --check succeeds and every feature is implemented.
    

    No feedback file was sent in this round.

    4 continue 14.3 min · 4 files changed, 1142 insertions(+), 444 deletions(-) Show prompt

    Prompt given to the harness:

    Read FEEDBACK.md in this directory — it lists problems a reviewer found when testing index.html in a real browser. Fix every item, keeping everything that already works, then run python3 build.py --check.
    

    FEEDBACK.md the harness received ✎ written by the AI reviewer:

    Reviewer feedback — round 2 (tested index.html in Chromium at 1440×900 and 390×844)

    Big improvement. The presets, per-class rates, quota panel, comparison pickers, task/model mismatch warning, multi-item sample with token chips and unit conversion all work. There are no console errors and no horizontal page scroll at 390 px. The items below are what I found by clicking through every feature and by actually running the generated Python script (against a stub openai module and a 3-item items.jsonl). Fix all of them and keep everything that already works.

    1. The generated Python script crashes on its first line of real work, for every task.

      • For Summarize, Classify, Extract and Translate, main() contains fn = embed_item if undefined else process → NameError: name 'undefined' is not defined.

      • For Embed it contains fn = embed_item if true else process → NameError: name 'true' is not defined. The JavaScript boolean is pasted into Python verbatim. Other problems in the same script:

      • response_format={"type": "json_object"} is sent for Summarize, Classify and Translate, although their prompts ask for plain text ("Reply with the label only").

      • For Extract it sends {"type": "json_object", "schema": '<a JSON string>'}, which is not a valid OpenAI response_format. The valid form is {"type": "json_schema", "json_schema": {"name": ..., "schema": {<a dict>}}}; alternatively use json_object with the schema described in the prompt.

      • The Embed path sends one item per request (truncated to 8,000 characters) instead of batching many inputs per request.

      • The aligned # comments are still ragged (for example CHUNK_TOK = 261834 #). Expected: every task's script runs to completion as generated, emits Python True/False (or simply defines the right function), uses response_format only for Extract, batches embedding inputs (for example 64 per request), and writes one result line per item. Please test the generated text by running it with Python, not just py_compile.

    2. Every Gantt lane is one identical full-width bar. At 4 parallel requests all 4 lanes are drawn from x=30 with width 602; at 32 parallel all 6 lanes are identical again. The chart therefore shows nothing beyond "total time".

      • The "100%" milestone label and the last axis tick ("2.9h") are clipped at the right edge.

      • Axis ticks mix units and odd values ("35.2m, 70.3m, 1.8h, 2.3h"). Expected:

      • Each lane is split into visible work blocks, for example 10 batches per lane with alternating shades. Each block shows its prompt-processing share and its generation share in two colours, with a small legend.

      • Lanes that get fewer items end earlier.

      • Ticks use "nice" steps in a single unit (0, 30 min, 1 h, 1.5 h …), and all labels stay inside the chart.

      • The hover tooltip on each block says which items it covers and when it starts and ends.

    3. Chart text scales with the panel, so it is unreadable on phones and oversized on desktop. The Gantt, tornado and chunking SVGs use a fixed viewBox that is stretched to the panel width:

      • At 390 px the Gantt lane labels ("W1"), axis ticks and milestone labels are about 5 px tall, and the chunking labels are the same.

      • At 1440 px the tornado labels are about 18 px, bigger than the panel headings. Expected: charts are drawn at their container's real pixel width, re-rendered on resize, with labels at a constant 11–13 px on every screen. On a phone, show fewer ticks and let lane labels sit above the bars if needed.

    4. The chunking visualizer is drawn wrongly and part of it is invisible.

      • The "fits in 4K / 8K / 32K / 131K / 262K" strip sits below the bottom of the SVG, so only its top edge shows.

      • With granite3-dense:8b (4,096-token window) and a 7,800-token item, the "one item" bar is the same width as the window bar. The item is capped to the panel width, so a 7,800-token item looks exactly as big as a 4,096-token window.

      • The capacity bar just fills solid teal, with no sign that the item overflows the window.

      • The "window 1 / window 2 / window 3" labels are dark text drawn over the teal fill, which is hard to read, and "window 3" collides with the fill edge.

      • There is no legend for teal, gold and grey.

      • With a 262K window and a 156-token item, the input is an invisible sliver in an empty beige bar. Expected:

      • Keep one consistent scale. When the item is larger than the window, the item spans the full width and the window bar is proportionally shorter.

      • Draw the overflow beyond the window edge, hatched or in red, labelled "3,124 tokens over".

      • Put labels outside the fills or in contrasting colours, add a legend, and make the fits strip fully visible.

      • When the item uses less than 1 % of the window, give segments a minimum visible width and add a callout such as "uses 0.4 % of the window".

    5. Class derivation is wrong for aliases, MoE models and ids without a size.

      • default-agent is an alias of qwen/qwen3.8-27b, yet it is classed "Large (70B+)" and estimates 4.5 h, while its identical target gets 2.1 h.

      • mixtral:8x7b is classed "Small (≤ 14B)", although 8×7B is about 47B parameters.

      • Ids with no size tag in their name (for example dots.MOCR) fall back to the context window, so any model with a 262K window becomes "Large" whatever its real size. Expected:

      • Aliases inherit their target's class, and the alias target is shown.

      • An NxMb tag is treated as N×M.

      • An unknown size becomes "Medium (size unknown — assumed)", visibly labelled.

    6. Presets leave stale sample information on screen, and the Items slider does not follow. Steps: paste a 3-item sample, then click "2 million abstracts → embed for search". The textarea is cleared, but "3 items · avg 645 tok · longest 1,860 tok ✓ avg sets totals" and all 40 token chips stay visible. The log Items slider stays at 40,000, and its readout still says "40,000" while the form says 2,000,000. Expected: a preset resets the sample stats and chips to the empty state and syncs every slider and readout. Also make the Items slider snap to round numbers: dragging produced "1,995,262" items, where 2,000,000 or 2.0M would be better.

    7. The "Generation tok/s ×" slider silently discards edits made in the rates table. Steps:

      1. Set the slider to 1.5×.

      2. Change Medium generation tok/s in the table to 100.

      3. Move the slider back to 1.0×. Result: the wall-clock time jumps from 30.9 min to 55.3 min, because an old snapshot of the rates is restored, yet the table still shows 100. Expected: the multiplier always applies on top of the current table values, and a table edit is never overwritten.

    8. The tornado chart is hard to read.

      • Bars are drawn proportional to absolute time on both sides instead of showing the change from the current estimate.

      • Only one end value is printed, there is no baseline label and no legend (what do purple and gold mean?), and rows are not sorted. Expected: a real tornado chart.

      • The centre line is the current estimate, labelled for example "now 9.5 h".

      • Bars extend left for faster and right for slower, by the change in time, with values at both ends (for example "−4 h 45 m" / "+9 h 30 m").

      • Rows are sorted by impact, with a small legend such as "assumption −50 % / +50 %".

    9. The most important comparison column is hidden, even on desktop. At 1440 px the TIME column of "Compare 3 models" is cut off at the panel edge: you can only see "TIM", "6.3" and "9.5". On a phone only Model, Class and Window are visible, with no hint that the table scrolls. The three pickers list all 73 models in one flat list. Expected:

      • Time is visible without scrolling at desktop width. Drop or merge less important columns: In tok and Out tok are identical across models, so show them once above the table.

      • On a phone, each model is a compact card with the time in large type and a fit pill.

      • The pickers use the same grouped optgroups as the main model picker.

    10. The quota panel is text only, and the defaults look like real figures. "100000000" is hard to read. Values like "11700%" and "448.5%" are noisy. The default scenario already shows a red "above your RPM limit" warning, but gives no concrete fix. Expected:

      • A visual budget meter: gold fill, with a red overflow segment past 100 %.

      • A formatted echo under the budget input ("= 100M tokens").

      • Labels saying the default budget and RPM are placeholders, not MindRouter quotas.

      • Whole-number percentages above 100 %, plus a "max concurrency within your RPM limit: N" suggestion, ideally with a one-click "apply".

    11. The token-chip preview splits words at a fixed 6 characters, which looks broken. It shows "Respon | dents", "descri | bed", "advisi | ng", "bookin | g". Real BPE tokenizers keep common words whole. Expected: keep ordinary words of up to about 10 letters whole, and split longer words at plausible boundaries (common suffixes such as -ing, -ed, -tion, -ment, -ly, -ness). The token counts should stay consistent with that, roughly 1.3 tokens per English word.

    12. Panel heights are unbalanced on desktop. The Timeline panel is short while the Sensitivity panel beside it is tall, leaving roughly 300 px of empty white card under the Gantt. Compare and Chunking have the same problem. Expected: give the Gantt its own full-width row (it deserves the width), or pair panels of similar height so there are no large empty card areas.

    6 continue 5.7 min · 4 files changed, 442 insertions(+), 226 deletions(-) Show prompt

    Prompt given to the harness:

    Read FEEDBACK.md in this directory — it lists problems a reviewer found when testing index.html in a real browser. Fix every item, keeping everything that already works, then run python3 build.py --check.
    

    FEEDBACK.md the harness received ✎ written by the AI reviewer:

    Reviewer feedback — round 4 (tested index.html in Chromium at 1440×900 and 390×844)

    Much better. All five generated Python scripts now run to completion; I ran them against a stub openai module. The rates-table/slider bug is fixed. Aliases inherit their class. Presets reset the sample and sync the sliders. The tornado chart reads well. The quota meter and the "apply N" button work, the phone comparison cards look good, and there is no page overflow at 390 px. The items below are what is left. Fix them all and keep everything that already works.

    1. Gantt timeline: every work block is squeezed into the first few pixels.

      • Preset "40,000 survey comments → classify" (4 parallel, 2.9 h total): all 80 block rectangles sit between x=40 and x=45 and are about 1 px wide. The four lanes look like a single thin green tick at 0 min, and the other 99 % of each lane is empty up to a "…" at the right edge.

      • At 32 parallel (22 min total) the blocks are 1 px vertical lines scattered across only the first ~5 minutes of a 22-minute axis, so the chart looks as if the job ends at 5 min.

      • At 10,000,000 items the whole chart is a column of ticks at "0 d", even though the axis runs to 10 d. Expected: each lane's blocks run continuously from 0 to that lane's finish time. Each block covers, for example, 10 % of the lane's calls, is visibly wide, has a 1–2 px gap to its neighbour, and shows its prompt/generation split. The last block of every lane ends near the 100 % marker, with shorter lanes ending a little earlier.

    2. Gantt labels collide and are unclear.

      • The legend ("prompt processing / generation") is drawn on top of the first axis tick label ("0 min" / "0 d"), on desktop and on phone.

      • The last tick reads "175.8 min" instead of a nice value in the same unit as the other ticks (for a 2.9 h job: 0, 30 min, 1 h … 3 h).

      • Every lane ends in an unexplained "…". Expected: put the legend above or below the chart with its own space, use nice rounded ticks throughout, and either remove the "…" or explain it in a tooltip.

    3. The chunking visualizer still gets the scale wrong when the item is bigger than the window. With granite3-dense:8b (4,096 window) and the "500 interview transcripts (20 pages)" preset (7,800-token item):

      • The "context window (4,096 tok)" bar is teal across the full width. There is a black tick at ~52 %, but the teal keeps going past the window edge with no hatching.

      • The "one item → 3 chunks" bar hatches part of chunk 2 and all of chunk 3 as "3,704 tokens over". After chunking nothing overflows, so this suggests chunks 2–3 are lost. With overlap 300 it hatches chunks 3 and 4. Expected:

      • The window bar is a box that ends at the window edge (about 52 % of the width here). The un-chunked item is drawn at the same scale, and the part past the edge is hatched red with the "N tokens over" label.

      • The "→ 3 chunks" row shows the item cut into chunks with no overflow hatching, each chunk fitting its window, with overlap shaded where chunks share tokens.

    4. Chunking labels overlap. The legend row (item input / output / prompt / overflow) is drawn over the bottom border of the "does one item fit?" boxes (4.1K ✕ … 262K ✓). On the phone the "window 1" label runs into the "does one item fit?" caption. Expected: leave clear vertical space between the rows and put the legend on its own line below the strip, at every width.

    5. The generated Python still has correctness issues in two tasks.

      • Extract: SCHEMA = {"fields": {"title": "string", ...}} is not a valid JSON Schema, so structured-output servers will not enforce it. Expected: a real schema such as {"type": "object", "properties": {"title": {"type": "string"}, "date": {"type": "string"}, "amount": {"type": "number"}}, "required": [...]}, with tidy indentation.

      • Embed:

        • Items are silently cut to 8,000 characters (it[:8000]) instead of being chunked to CHUNK_TOK, so long items lose text without warning.

        • The final message prints the number of batches as "result lines": with 3 items it says "Wrote 1 result lines".

        • The embed script also carries unused leftovers (process(), embed_item(), PROMPT = '{text}', MAX_TOKENS = 1). Expected: chunk long items (one vector per chunk, recorded with its item index and chunk number), count and print real result lines, and include only the functions the chosen task uses.

    6. The tornado chart shows empty rows for the embed task. With "Embed for search", the "Gen tok/s" and "Output length" rows read "+0 s | +0 s" with no bars. Expected: hide rows that cannot affect the chosen task, or grey them out with a short "no effect for embeddings" note.

    7. The Compare panel leaves a large empty area on desktop. At 1440 px the Compare 3 models card is about 200 px shorter than the Sensitivity card beside it, so there is a big blank area under the table. Expected: use that space for something useful and visual. For example, a small horizontal bar chart of the three models' estimated times (and chunks per item), with the selected model highlighted and the fastest labelled, animating when the pickers change.

    8. The quota meter label is hard to read when over budget. When the budget is exceeded, the "449%" label is dark text on a saturated red bar. Expected: put the percentage label beside the bar, or use white text with enough contrast, and mark the 100 % point on the track.

    11 continue 0.8 min · — Show prompt

    Prompt given to the harness:

    Read FEEDBACK.md in this directory — it lists problems a reviewer found when testing index.html in a real browser. Fix every item, keeping everything that already works, then run python3 build.py --check.
    

    No feedback file was sent in this round.

    12 continue 14.6 min · — Show prompt

    Prompt given to the harness:

    Read FEEDBACK.md in this directory — it lists problems a reviewer found when testing index.html in a real browser. Fix every item, keeping everything that already works, then run python3 build.py --check.
    

    No feedback file was sent in this round.

    13 critic 6.7 min · 5 files changed, 473 insertions(+), 166 deletions(-) Show prompt

    Prompt given to the harness:

    Read FEEDBACK.md in this directory — it lists problems a reviewer found when testing index.html in a real browser. Fix every item, keeping everything that already works, then run python3 build.py --check.
    

    FEEDBACK.md the harness received ✎ written by the AI reviewer:

    Reviewer feedback — critic pass (tested index.html in Chromium at 1440×900 and 390×844)

    The app looks good. It has zero console errors, and all five Python scripts parse. The main estimates match a hand calculation: the default is 9.5 h, 8.6M tokens and 40,000 calls. But a fresh review found real calculation bugs. Fix the items in order, since the most important come first. Keep everything else that works, and run python3 build.py --check at the end.

    1. Empty state shows stale results underneath. Set "Number of items" to 0 (or the length to 0). The "Describe your corpus to see an estimate" card appears, but every old tile, chart and the snippet stay visible below it. The cause is .results__body { display: flex; } in src/app.css, which overrides the hidden attribute. Fix: add [hidden] { display: none !important; } near the top of src/app.css.

    2. The fit check uses the average sample item, not the longest item, although the UI says "longest drives fit". To reproduce:

      • Choose aya-expanse:8b and Summarize.

      • Paste a short sentence, a blank line, then a 7,000-word item such as the word "research" repeated.

      • The fit panel says "Fits in one call. 3,504 input + 526 output …", and the chunk diagram says "one item (7,000 tok) → 1 chunk".

      • But the longest item needs 7,000 + 1,050 output + 150 + 100 = 8,300 tokens, which is more than 8,192, so it needs 2 chunks.

        Cause: estimate() calls fitFor(model, perItemInputTokens(), …), which uses the average. Fix: also compute a fit with longestItemTokens(). Use that fit for:

      • renderFit

      • renderChunking, including the per-call output drawn in the diagram

      • the comparison table's "Fits?" pill

        Totals may stay on the average.

    3. The Sensitivity tornado's "Output length" row is always "+0 s / +0 s" for every task, even Translate, where output dominates. Cause: est2() in renderSensitivity ignores o.outMul, because fitFor() and outForChunk() read state.outMul. Fix: inside est2, set state.outMul = o.outMul, compute, then restore the old value. You could instead pass the multiplier as a parameter. Expected: with the "500 interview transcripts" preset and Translate, ±50% output length changes the time by hours.

    4. Total tokens and the quota ignore the per-call prompt overhead and the chunk overlap, so the quota is badly underestimated.

      • In the default view, 40,000 calls × 150 prompt tokens is 6.0M tokens that are sent but never counted. The app says 8.6M; the real figure is about 14.6M.

      • Fix: totalIn = items × inTok + calls × promptOverhead + items × (chunks − 1) × overlap.

      • Use it in the tiles, the hero headline, the quota panel, the sensitivity readout and the comparison.

      • Change the input tile's sub-label to something like "156 / item + 150 prompt / call".

      • Chunk counts differ per model, so add a "Tokens" column to the comparison table and remove the "(same for all three)" line.

    5. The embedding estimate disagrees with the generated script. The estimate assumes one API call per item, each with a 150-token instruction prompt and 0.5 s overhead. The "2 million abstracts" preset shows 2,000,000 calls, 402 req/min, and "Above your RPM limit" with no fix offered. The script, however, sends EMBED_BATCH = 64 inputs per request. Fix, for the Embed task only:

      • Use no instruction-prompt overhead: treat it as 0 in fitFor, estimate and the fit message.

      • Set requests = ceil(items × chunks ÷ 64).

      • Set time per request = (64 × chunk input tokens) ÷ the embedding prompt rate + overhead.

      • Base the RPM check and the timeline on requests.

      • Show the Embedding row's "Generation tok/s" cell as "n/a" (a disabled input).

    6. The comparison table compares the wrong kind of model. In the Embed task it shows chat models (e.g. aya-expanse:8b "fits · 4 d 18 h") and can crown one "fastest". Fix: call compatWarning(m) for each row. When it returns a warning, show a grey "wrong type" pill in the Fits? column and "—" for the time. Exclude that row from "fastest" and from the bar chart. Apply the same rule to embedding models under text tasks.

    7. The "window too small" state still shows nonsense values. To reproduce, choose granite3-dense:8b and set "Prompt / instruction overhead" to 5000. You get:

      • Hero headline "≈ 0 s on MindRouter · … · 0 calls", time tile "0 s", "0 API calls".

      • Quota lines "Rate: 2583B tok/h" and "Within your RPM limit".

      • Timeline milestone labels piled up as "100%25%" at 0 s.

      • Every tornado bar "+0 s".

      • Python CHUNK_TOK = 0, which would send one word per call.

        Fix: at the top of update(), if the selected model's fit has tooSmall, render a no-estimate state:

      • Hero: "No estimate — this model's window is too small for this task".

      • Time tile and API-calls sub-label: "—".

      • Quota readout: that one sentence instead of the rate and RPM lines.

      • #timeline and #tornado: a single <p> with that sentence instead of the SVGs.

      • Snippet: only a Python comment saying the window is too small, and to pick a larger-context model.

        Everything returns to normal when a fitting model or overhead is chosen.

    8. Task output lengths contradict the snippet prompts.

      • Classify scales output with item length (outRatio 0.02): a 20-page transcript gets 156 output tokens and MAX_TOKENS = 234, yet the prompt says "Reply with the label only". Fix: make Classify a fixed output with outRatio: 0 and minOut: 12.

      • Summarize asks for "2-3 sentences" while budgeting 1,170 output tokens. Fix: generate the prompt as "Summarize the following item in about N words", where N = round(per-call output tokens × 0.75).

    9. CHUNK_TOK in the Python snippet is an unrounded float (227681.7391304348). Use Math.floor(est.fit.avail), and keep the # comments aligned.

    10. The chunking diagram is blank for short items. In the default view (a 156-token item in a 262,144-token window), the "context window" bar and "window 1" are empty, because every segment is under one pixel wide. Fix: when prompt + input + output + margin is under 5% of the window, draw each used segment at least 6 px wide. Add the caption "(small segments enlarged to be visible — not to scale)".

    11. The first page load shows a red "Above your RPM limit" warning based on an invented placeholder of 60. Make the RPM field empty by default (state.rpm = 0) with the placeholder text "e.g. 60". When it is empty, show only "Implied request rate: 70.4 req/min — enter your limit to check it." The warning and the "apply N" button keep working once the user enters a limit.

    12. Wording and small visual nits:

      • (a) Replace "MindRouter does not publish tokens/second, pricing or quota data" in the How-to note, the card intro and the snippet's closing NOTE. Use "MindRouter's model catalog does not include throughput, pricing or quota figures". Replace "match your own cluster" with "match what you observe on MindRouter".

      • (b) Under "Parallel requests", add the hint "Assumes each request keeps its speed as concurrency rises — real servers slow down under heavy load."

      • (c) Use US spelling in How to use: "summarize".

      • (d) Timeline legend: on a 390 px phone the "generation" swatch touches the words "prompt processing". Start the second legend item at left + 140.

      • (e) On the phone, the gold "selected" bar in the comparison cards overlaps the "MODEL" label text. Give the card extra left padding, or move the bar outside the text.

    14 critic 19.0 min · 1 file changed, 23 insertions(+), 60 deletions(-) Show prompt

    Prompt given to the harness:

    Read FEEDBACK.md in this directory — it lists problems a reviewer found when testing index.html in a real browser. Fix every item, keeping everything that already works, then run python3 build.py --check.
    

    FEEDBACK.md the harness received ✎ written by the AI reviewer:

    Reviewer feedback — critic pass 2 (tested index.html in Chromium at 1440×900 and 390×844)

    Great work on round 13: all 12 items were verified in the browser. The empty state hides stale results, the longest item drives the fit check, embeddings are batched (31,250 requests), wrong-type rows are marked, the too-small state is clean, and the RPM field is optional. A few problems remain, including one new regression. Fix these in order, change nothing else, then run python3 build.py --check.

    1. The Sensitivity tornado is wrong whenever the "Output length" slider is not 1×, and it throws console errors. To reproduce, open the default view and drag the "Output length" slider to 2×:

      • The console shows <rect> attribute width: A negative value is not valid. ("-36.7").

      • The Items row shows "−11 h 24 m ; −1.9 h", with both sides negative.

      • Gen tok/s shows "+0 s".

      • At 0.5×, the Concurrency "faster" side shows "+10.6 min".

        Cause: est2() in renderSensitivity does var outMul = o.outMul || 1; state.outMul = outMul;, which is an absolute multiplier. Rows that do not change output are computed at 1×, not at the slider's value. Fix:

       var curOut = state.outMul || 1;
       var outMul = o.outMul !== undefined ? curOut * o.outMul : curOut;

    The Output length row keeps passing 0.5 and 1.5 as relative factors. Also clamp both bar widths with Math.max(0, …) so a negative width can never be written.

    1. The input tile does not add up with the total. The default view shows Total input 6.2M + Total output 2.4M, but Total tokens 15M. The input tile and the "In … · Out …" line above the comparison table leave out the prompt tokens. Fix: show input = totalTok − totalOut, which is items × input + calls × prompt overhead + the re-sent overlap. Keep the sub-label "156 / item + 150 prompt / call". Expected default: Input 12M, Output 2.4M, Total 15M.

    2. The time format can print "3 d 24 h" or "10 h 60 m". "3 d 24 h" appeared in the tornado with 40-page items, aya-expanse:8b and overlap 300. In fmtTime and fmtDelta:

      • When the rounded hours (hh2) reach 24, add one day and set the hours to 0.

      • When the rounded minutes (mm) reach 60, add one hour and set the minutes to 0.

    3. Row 3 of the chunking diagram ("window 1") is still an empty bar in the default view (a 156-token item in a 262,144-token window). Row 1 now enlarges small segments. Apply the same rule to the per-chunk windows in row 3: when the used share is under 5%, draw the input, prompt, output and margin segments at least 6 px wide.

    4. The caption is cut off on the phone. At 390 px, "context window (262,144 tok) (small segments enlarged to be visible — not to scale)" runs past the right edge. When the SVG is narrower than 560 px, draw "(small segments enlarged — not to scale)" as a second caption line below the first. You can shorten the note instead.

    5. On the phone, the comparison card's gold selection bar still overlaps the "MODEL" label. It comes from .cmp tbody tr.is-current td:first-child { box-shadow: inset 3px 0 0 var(--ui-pride-gold); }. Inside the phone @media block, set box-shadow: none for that selector, since the gold card border already marks the selection.

    6. Small cleanups:

      • For the Embed task the hero headline says "8 calls" while the tile says "8 requests". Use "requests" in the hero for embeddings too.

      • Remove the duplicated .pill--na and .cmp tbody tr.is-wrong rules in src/app.css.

    15 continue 2.3 min · 3 files changed, 78 insertions(+), 30 deletions(-) Show prompt

    Prompt given to the harness:

    Read FEEDBACK.md in this directory — it lists problems a reviewer found when testing index.html in a real browser. Fix every item, keeping everything that already works, then run python3 build.py --check.
    

    FEEDBACK.md the harness received ✎ written by the AI reviewer:

    Reviewer feedback — critic pass 2 (tested index.html in Chromium at 1440×900 and 390×844)

    Great work on round 13: all 12 items were verified in the browser. The empty state hides stale results, the longest item drives the fit check, embeddings are batched (31,250 requests), wrong-type rows are marked, the too-small state is clean, and the RPM field is optional. A few problems remain, including one new regression. Fix these in order, change nothing else, then run python3 build.py --check.

    1. The Sensitivity tornado is wrong whenever the "Output length" slider is not 1×, and it throws console errors. To reproduce, open the default view and drag the "Output length" slider to 2×:

      • The console shows <rect> attribute width: A negative value is not valid. ("-36.7").

      • The Items row shows "−11 h 24 m ; −1.9 h", with both sides negative.

      • Gen tok/s shows "+0 s".

      • At 0.5×, the Concurrency "faster" side shows "+10.6 min".

        Cause: est2() in renderSensitivity does var outMul = o.outMul || 1; state.outMul = outMul;, which is an absolute multiplier. Rows that do not change output are computed at 1×, not at the slider's value. Fix:

       var curOut = state.outMul || 1;
       var outMul = o.outMul !== undefined ? curOut * o.outMul : curOut;

    The Output length row keeps passing 0.5 and 1.5 as relative factors. Also clamp both bar widths with Math.max(0, …) so a negative width can never be written.

    1. The input tile does not add up with the total. The default view shows Total input 6.2M + Total output 2.4M, but Total tokens 15M. The input tile and the "In … · Out …" line above the comparison table leave out the prompt tokens. Fix: show input = totalTok − totalOut, which is items × input + calls × prompt overhead + the re-sent overlap. Keep the sub-label "156 / item + 150 prompt / call". Expected default: Input 12M, Output 2.4M, Total 15M.

    2. The time format can print "3 d 24 h" or "10 h 60 m". "3 d 24 h" appeared in the tornado with 40-page items, aya-expanse:8b and overlap 300. In fmtTime and fmtDelta:

      • When the rounded hours (hh2) reach 24, add one day and set the hours to 0.

      • When the rounded minutes (mm) reach 60, add one hour and set the minutes to 0.

    3. Row 3 of the chunking diagram ("window 1") is still an empty bar in the default view (a 156-token item in a 262,144-token window). Row 1 now enlarges small segments. Apply the same rule to the per-chunk windows in row 3: when the used share is under 5%, draw the input, prompt, output and margin segments at least 6 px wide.

    4. The caption is cut off on the phone. At 390 px, "context window (262,144 tok) (small segments enlarged to be visible — not to scale)" runs past the right edge. When the SVG is narrower than 560 px, draw "(small segments enlarged — not to scale)" as a second caption line below the first. You can shorten the note instead.

    5. On the phone, the comparison card's gold selection bar still overlaps the "MODEL" label. It comes from .cmp tbody tr.is-current td:first-child { box-shadow: inset 3px 0 0 var(--ui-pride-gold); }. Inside the phone @media block, set box-shadow: none for that selector, since the gold card border already marks the selection.

    6. Small cleanups:

      • For the Embed task the hero headline says "8 calls" while the tile says "8 requests". Use "requests" in the hero for embeddings too.

      • Remove the duplicated .pill--na and .cmp tbody tr.is-wrong rules in src/app.css.

What the agent was asked

The full specification the harness received, rendered from the original Markdown.

Read the specification

AI Workload Estimator — Plan an LLM-assisted research project on MindRouter

Category: Research Who it is for: Researchers planning to process documents, transcripts, survey responses or code with AI

What it should do

Planning to have an AI read 40,000 survey comments or 2 million abstracts? Estimate the tokens, wall-clock time and quota you will need on MindRouter before you start — and pick the right model.

Data

A snapshot of MindRouter's live model catalog (GET https://mindrouter.uidaho.edu/v1/models — id, context_length, capabilities) saved at build time, plus editable throughput assumptions (tokens/second per model class, parallel requests) clearly labeled as estimates.

Raw downloads (if any) are in data/raw/ with data/raw/SOURCES.md describing where each file came from. Transform what the app needs into compact JSON in data/embed/ (write a small script in scripts/ to do it, so the transformation is reproducible). Every dataset the app shows must be credited in a visible "Data & sources" section of the app, with links. If a dataset is illustrative/synthetic, label it clearly as such in the UI — never present made-up numbers as real University of Idaho figures.

Required features

  • Corpus description form: number of items, average length (words, pages or characters), with a paste-sample box that measures a real sample's tokens (approximate tokenizer in JS)

  • Task templates (summarize, classify, extract fields to JSON, translate, embed for search) that set typical output lengths

  • Model picker from the catalog snapshot with context-window fit checks (does one item fit? how many chunks?)

  • Results: total input/output tokens, estimated time at chosen concurrency, and a Gantt-like timeline; sensitivity sliders

  • Comparison table across 3 models and a chunking visualizer

  • Copyable Python batching snippet that calls MindRouter's OpenAI-compatible endpoint with the chosen settings

Quality bar

This app is going into a University of Idaho gallery that shows faculty, staff and students what they can build with AI coding agents. It must look polished and modern (think a well-designed data product, not a homework demo), feel alive (smooth transitions, responsive hover/selection states, animated where it helps understanding), and do real, useful work — every control should change something meaningful. It must work well on a laptop and on a phone. Include a short "How to use" hint for first-time visitors and a helpful empty state wherever the user must supply data.

Known limitations

Reported by the independent critic. ✎ written by the AI critic

  • With a pasted sample of mixed lengths, the fit check and chunk diagram use the longest item, but totals and API calls use the average item (1 chunk). A sample whose longest item needs 2 chunks still shows calls = items.
  • Throughput defaults (tok/s per model class) are illustrative estimates, and wall time assumes a request keeps the same speed as concurrency rises. Both are labeled in the UI.
  • In the 'window too small' state, the 'In ... · Out ...' line above the comparison table still shows token totals, while the rest of the results show 'No estimate'.
  • The disabled 'Generation tok/s' input for the Embedding row renders as an empty box, not as 'n/a' text.
  • The Summarize prompt scales with item length (e.g. 'in about 878 words' for 20-page transcripts). It is consistent with the estimate, but long for a summary.
  • Harness reliability: critic round 14 looped the same grep more than 143 times without edits and was killed; the identical prompt then succeeded as round 15 (continue) in 140 s.