anomalyco / anomalyco/models.dev
web: Select columns to display and improve web performance
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 6.9k
- Forks
- 1.7k
- Avg merge
- 3h 21m
- Merged PRs (30d)
- 873
Description
And the filter shouldn't match the dollar cost...
Maybe use Tanstack Table:
https://tanstack.com/table/latest/docs/framework/vanilla/examples/basic?panel=sandbox
It can be used without React
https://tanstack.com/table/latest/docs/vanilla
Proposal
Design: Tanstack Table Refactor — models.dev
Summary
Refactor the models.dev web table from a statically-rendered HTML table with hand-rolled DOM sort/filter to a fully client-side Tanstack Table implementation with virtual scrolling and column selection. The build pipeline and TOML data files remain unchanged.
Goals
- Column picker: users can show/hide any of the 24 columns
- Performance: virtual scrolling reduces DOM nodes from ~25,000 to ~750
- Cleaner code: replace manual DOM sort/filter with Tanstack Table state
- URL-sharable state: sort, filter, and visible columns encoded in URL params
Non-goals
- No changes to TOML data files or the
providers/directory - No changes to
script/build.ts,server.ts, orindex.html - No React or other framework introduced
Architecture
Build time (unchanged)
providers/**/*.toml → packages/core → api.json + _index.html
The build step in script/build.ts continues to generate api.json from all TOML files. This JSON file is the data source for the client.
Runtime (new)
Page load (thin HTML shell)
↓
fetch /api.json
↓
Flatten: providers[].models[] → Row[]
↓
@tanstack/table-core (sorting, filtering, column visibility)
↓
@tanstack/virtual-core (only render ~30–40 visible rows)
↓
Render <table> rows to DOM
↓
URL params updated on any state change
Data Model
The api.json structure (already generated) has shape:
{
[providerId: string]: {
name: string,
models: {
[modelId: string]: {
name: string,
family?: string,
cost?: { input, output, reasoning, cache_read, cache_write, input_audio, output_audio },
limit: { context, input?, output },
modalities: { input: string[], output: string[] },
tool_call: boolean,
reasoning: boolean,
structured_output?: boolean,
temperature: boolean,
open_weights: boolean,
knowledge?: string,
release_date?: string,
last_updated?: string,
}
}
}
}
Flattened into a Row[] array, one entry per model, adding providerId and providerName fields.
Column Definitions (24 columns)
All columns are visible by default. Each column has an id (used in URL params).
| ID | Label | Data type | Default visible |
|---|---|---|---|
provider |
Provider | text | ✅ |
model |
Model | text | ✅ |
family |
Family | text | ✅ |
provider-id |
Provider ID | text (mono) | ✅ |
model-id |
Model ID | text (mono) + copy | ✅ |
tool-call |
Tool Call | boolean | ✅ |
reasoning |
Reasoning | boolean | ✅ |
input-modalities |
Input | modalities | ✅ |
output-modalities |
Output | modalities | ✅ |
input-cost |
Input Cost | cost ($/1M) | ✅ |
output-cost |
Output Cost | cost ($/1M) | ✅ |
reasoning-cost |
Reasoning Cost | cost ($/1M) | ✅ |
cache-read-cost |
Cache Read | cost ($/1M) | ✅ |
cache-write-cost |
Cache Write | cost ($/1M) | ✅ |
audio-input-cost |
Audio Input | cost ($/1M) | ✅ |
audio-output-cost |
Audio Output | cost ($/1M) | ✅ |
context-limit |
Context Limit | number | ✅ |
input-limit |
Input Limit | number | ✅ |
output-limit |
Output Limit | number | ✅ |
structured-output |
Structured Output | boolean | ✅ |
temperature |
Temperature | boolean | ✅ |
weights |
Weights | text | ✅ |
knowledge |
Knowledge | text | ✅ |
release-date |
Release Date | text | ✅ |
last-updated |
Last Updated | text | ✅ |
URL State
All interactive state is encoded in URL query params. State is synced bidirectionally (URL → table on load, table → URL on any change). Browser back/forward restores previous state.
?search=gpt&sort=input-cost&order=asc&cols=provider,model,input-cost,output-cost
| Param | Meaning | Default (when absent) |
|---|---|---|
search |
Text filter (comma-separated OR) | empty |
sort |
Column ID to sort by | none |
order |
asc or desc |
asc |
cols |
Comma-separated visible column IDs | all columns |
When cols is absent, all columns are shown. When cols is present, only the listed columns are visible.
Virtual Scroll
The table body lives inside a fixed-height scroll container:
height: calc(100vh - var(--header-height) - var(--thead-height))
overflow-y: auto
@tanstack/virtual-core creates a Virtualizer instance bound to this container. It calculates which row indices are in view and provides virtualItems with their start offset and size.
Rows are absolutely positioned inside a tall outer <tbody> using transform: translateY(${item.start}px). Estimated row height: 45px (matches current CSS). Overscan: 5 rows above and below viewport.
On sort or filter, the virtualizer scrolls back to the top.
Column Picker UI
A "Columns" button is added to the header (next to the search box and "How to use" button). Clicking it opens a dropdown panel anchored below the button.
The panel groups columns into 5 sections with checkboxes:
- Identity: Provider, Model, Family, Provider ID, Model ID
- Capabilities: Tool Call, Reasoning, Structured Output, Temperature, Weights
- Modalities: Input, Output
- Cost: Input, Output, Reasoning, Cache Read, Cache Write, Audio In, Audio Out
- Limits: Context, Input, Output
- Metadata: Knowledge, Release Date, Last Updated
"Show all" / "Reset" links appear at the top of the panel. The panel closes on outside click or Escape.
Files Changed
| File | Change |
|---|---|
packages/web/src/render.tsx |
Remove all row/tbody JSX. Keep header, empty table shell, column picker button placeholder |
packages/web/src/index.ts |
Full rewrite: fetch api.json, Tanstack Table + Virtual, column picker, URL sync |
packages/web/src/index.css |
Add column picker dropdown styles, virtual scroll container, loading state |
packages/web/package.json |
Add @tanstack/table-core, @tanstack/virtual-core |
Files not changed: script/build.ts, src/server.ts, index.html, all TOML files, providers/ directory.
Performance Comparison
| Metric | Before | After |
|---|---|---|
| DOM nodes (1000 rows × 25 cols) | ~25,000 <td> |
~750 <td> (~30 rows) |
| Sort | DOM re-order of all rows | Tanstack state + re-render 30 rows |
| Filter | display: none on all non-matching rows |
Tanstack filter + re-render 30 rows |
| Column hide | N/A | CSS + DOM update |
| Initial render | HTML baked at build time | fetch api.json + JS render |
The trade-off: initial render requires JS (fetch + Tanstack init). A skeleton/loading state will show while data loads.
Loading State
While api.json is being fetched, a skeleton loading indicator shows in the table body. Given api.json is served from the same origin, this should complete in <100ms in practice.
Packages to Add
"@tanstack/table-core": "^8",
"@tanstack/virtual-core": "^3"
Both are framework-agnostic (vanilla JS). No React, no other deps.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with packages/web/src/index.ts, packages/web/src/render.tsx, packages/web/src/index.css, and packages/web/package.json to understand the current table and build setup. Verify the proposed client-side table, virtual scrolling, column picker, cost-aware filtering, and URL state against the stated goals, while leaving the listed build and data files unchanged. Done means all 24 columns, sorting, filtering, virtualization, loading, and URL synchronization work as specified.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- frontend, performance, web-dev
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Clearly specified
- Newbie friendliness
- 35/100