feature-bay
Which LLMs actually support what — live from OpenRouter.
A live, side-by-side capability matrix of every LLM API on OpenRouter:
which models / providers actually support tools, structured outputs,
reasoning tokens, web search, vision, audio, prompt caching,
JSON mode, seed, logprobs, 1M+ context, free tier, and more.
Refreshed continuously from public unauthenticated endpoints.
Built because every team has someone wasting a Slack thread each week on
"does Claude Sonnet 4.6 support structured outputs via Together vs DeepInfra
vs Anthropic direct?" — and no public dashboard groups it by feature,
lets you filter, or surfaces drift.
Live URL
When deployed via RNDLAB: https://holyai.me/feature-bay/
Endpoints
| Method | Path | Returns |
|---|---|---|
| GET | /feature-bay/health | { ok, version, models, endpoints, last_refresh } |
| GET | /feature-bay/api/features | the canonical feature catalogue |
| GET | /feature-bay/api/matrix?feature=tools&feature=vision_in&match=all&free=1&context_min=200000&provider=Together&q=llama | filtered matrix rows |
| GET | /feature-bay/api/model/<owner>/<slug> | one model's detail + per-provider feature breakdown |
| GET | /feature-bay/api/feature/<key> | every model+provider supporting one feature, plus cheapest/newest |
| GET | /feature-bay/api/providers | per-provider scorecard |
| GET | /feature-bay/api/movers?window=7d | feature gains/losses |
| GET | /feature-bay/api/refresh/status | last refresh times + last 20 log rows |
| POST | /feature-bay/api/refresh/models | force-refresh the catalogue (rate-limited 1/60s/IP, no auth) |
| POST | /feature-bay/api/refresh/endpoints?model=<id> | force-refresh one model's endpoints (rate-limited 1/60s/IP, no auth) |
| GET | /feature-bay/feed.json | JSON Feed 1.1 of recent feature changes |
| GET | /feature-bay/model/<id> | server-rendered model page (OG tags) |
| GET | /feature-bay/feature/<key> | server-rendered feature page (OG tags) |
| GET | /feature-bay/ | SPA shell |
Real data sources — NO mock data, no seed, no Math.random()
| Source | URL | Refresh | Notes |
|---|---|---|---|
| OpenRouter Models catalogue | https://openrouter.ai/api/v1/models | every 15 min via node-cron | top-level supported_parameters, pricing, architecture.input_modalities, architecture.output_modalities, context_length, tokenizer, etc. |
| OpenRouter per-model endpoints | https://openrouter.ai/api/v1/models/{author}/{slug}/endpoints | round-robin every minute, full coverage every ~60 min | per-provider supported_parameters, pricing, uptime_last_30m, status, tag (quantization) |
The optional OPENROUTER_API_KEY env var is sent as Authorization: Bearer …
only to raise rate limits — the product runs end-to-end with no key set.
If a fetch fails, the row is left stale and refresh_log records the error.
The UI surfaces per-row freshness. No fake fallback rows are ever inserted.
Deterministic feature derivation
Every feature is derived from the OpenRouter payload using a deterministic
rule — no LLM classification, no guessing. The exhaustive list lives inlib/features.js; the rules are:
| Feature | Rule |
|---|---|
| tools | supported_parameters includes tools |
| tool_choice | supported_parameters includes tool_choice |
| structured_outputs | supported_parameters includes structured_outputs OR response_format |
| json_mode | supported_parameters includes response_format |
| reasoning | supported_parameters includes reasoning OR include_reasoning, OR pricing.internal_reasoning is not null |
| web_search | supported_parameters includes web_search_options OR pricing.web_search is not null |
| vision_in | architecture.input_modalities includes image |
| audio_in | architecture.input_modalities includes audio |
| audio_out | architecture.output_modalities includes audio |
| file_in | architecture.input_modalities includes file |
| prompt_caching | pricing.input_cache_read is not null |
| seed, logprobs, logit_bias, stop_sequences, min_p, top_k, frequency_penalty, presence_penalty | matching key in supported_parameters |
| context_1m | context_length >= 1_000_000 |
| context_200k | context_length >= 200_000 |
| free_tier | pricing.prompt === "0" AND pricing.completion === "0" |
Architecture
- Node.js 20 + Express 4 ESM
- better-sqlite3 in WAL mode (
data/feature-bay.db) - node-cron for the three refresh schedules
- helmet + compression + morgan
- Vanilla JS SPA, dark theme, no framework, no bundler
- All routes mounted under
BASE_PATH = /feature-bay(env-override-able)
Auth
None. Every endpoint — read and write — is public. There is norequireAuth, no Basic Auth, no ADMIN_PASS, no /admin login. The POST/api/refresh/* endpoints are rate-limited by IP only (1 req / 60 s).
Local dev
npm install
node server.js
# → http://localhost:4812/feature-bay/
The first cron tick (within 1s of boot) populates the catalogue. After
~60 minutes the round-robin endpoints refresh has covered every model.
Manual refreshes
npm run refresh:models
npm run refresh:endpoints
npm run diff:once
Deploy
DEPLOY_MANIFEST.json follows the RNDLAB orchestrator schema. The Mac-mini
watcher picks up cowork-deploy-bridge/queue.jsonl and handles the rest
(rsync → systemd → nginx → showcase → Playwright thumbnail).
Licence
MIT.