docs: add filterable AI models catalog snapshot#43
Conversation
Surface the live ai_models catalog (84 models, 16 providers) on the Available Models page as a client-side filterable table. Snapshot is regenerated from Supabase via `pnpm gen:models`; credentials come from the existing NEXT_PUBLIC_SUPABASE_* Vercel env vars. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Mogplex PR Review
Status: Attention needed
This PR adds a well-structured, client-side filterable AI models catalog to the docs. The component logic is solid and the script is clean. There is one security concern worth discussing: the Supabase project URL (enxvgkxsrpbtqlaeotjz.supabase.co) is hardcoded inside the committed ai-models.json as the source field. While the PR notes and description correctly acknowledge that the anon key is public-scope, permanently embedding an identifiable project reference in the git history is worth a deliberate choice rather than an accident. Additionally, there's a minor correctness issue with the formatPrice helper for sub-$0.001 values, and a missing no-pagination guard in the generator script. Overall the PR is close to merge-ready with one intentional decision to confirm and two small fixes.
4 findings were added inline.
| @@ -0,0 +1,1710 @@ | |||
| { | |||
| "source": "https://enxvgkxsrpbtqlaeotjz.supabase.co/rest/v1/ai_models", | |||
There was a problem hiding this comment.
Warning: Supabase project URL permanently embedded in committed JSON
The source field is "https://enxvgkxsrpbtqlaeotjz.supabase.co/rest/v1/ai_models" — the full project reference URL is now in git history forever. The PR description acknowledges the anon key is public, but the project ID (enxvgkxsrpbtqlaeotjz) is also now permanently disclosed in the repo, regardless of whether the key is ever rotated or the table ACLs change.
This may be completely intentional (the docs site is public, the anon key is intended to be public, and the URL is derivable anyway from NEXT_PUBLIC_SUPABASE_URL). If so, a brief comment in AGENTS.md or the PR description confirming this is acceptable would silence the concern for future readers.
If you'd prefer not to commit it, you can strip the source field from the JSON snapshot in the generator:
const snapshot = {
fetched_at: new Date().toISOString(),
count: rows.length,
models: rows,
};The URL is not consumed by the component (ai-models-table.tsx never reads snapshot.source), so removing it is purely cosmetic.
| async function main() { | ||
| const url = new URL(`${SUPABASE_URL}/rest/v1/ai_models`); | ||
| url.searchParams.set('select', SELECT); | ||
| url.searchParams.set('is_hidden', 'eq.false'); |
There was a problem hiding this comment.
Warning: Generator has no pagination — silently truncates at Supabase's default row limit
The Supabase REST API returns a maximum of 1000 rows by default (configurable per project, but often set lower). The script makes a single fetch with no limit override and no check that rows.length equals the expected total. If the catalog ever grows past the project's page size, the snapshot will silently contain a partial list with count reflecting only what was returned.
Fix: either set an explicit high limit, or assert that the response isn't suspiciously truncated:
url.searchParams.set('limit', '10000'); // Supabase max is configurable; set high
// After fetch:
if (rows.length === 0) throw new Error('No rows returned — check RLS / filters');
// Optionally: warn if near a round boundary that might indicate truncation
if (rows.length % 100 === 0) {
console.warn(`Received exactly ${rows.length} rows — may be hitting a page limit.`);
}| function formatPrice(value: number | null): string { | ||
| if (value == null) return '—'; | ||
| const perMtok = value * 1_000_000; | ||
| if (perMtok >= 1) return `$${perMtok.toFixed(2)}`; |
There was a problem hiding this comment.
Suggestion: formatPrice produces $0.00 for very small values instead of a more useful string
For a pricing_input like 7e-8 (e.g. zai/glm-4.7-flash), perMtok = 7e-8 * 1_000_000 = 0.07. Since 0.07 < 1, the code falls into the toFixed(3) branch and produces "$0.07" — correct.
However, for a value of exactly 1e-9, perMtok = 0.001 and toFixed(3) gives "$0.001", then the regex strips trailing zeros leaving "$0.001" — fine. For 1e-10, perMtok = 0.0001 and toFixed(3) rounds to "$0.000", then both regexes strip it entirely to "$" — a broken output string.
No entry in the current snapshot triggers this, but it's a latent bug if a free/very-cheap model is added. Consider clamping to a minimum precision:
function formatPrice(value: number | null): string {
if (value == null) return '—';
const perMtok = value * 1_000_000;
if (perMtok === 0) return '$0';
if (perMtok >= 1) return `$${perMtok.toFixed(2)}`;
if (perMtok >= 0.001) return `$${perMtok.toFixed(3).replace(/0+$/, '').replace(/\.$/, '')}`;
return `$${perMtok.toExponential(1)}`; // e.g. "$1.0e-4"
}| label="Provider" | ||
| options={allProviders} | ||
| selected={providers} | ||
| onToggle={(v) => setProviders((s) => toggle(s, v))} |
There was a problem hiding this comment.
Suggestion: DATA.count may not equal filtered.length — the footer copy could confuse readers
DATA.count is the value written by the generator at snapshot time (84). The footer reads "Showing X of 84 models". If someone reruns gen:models and the catalog shrinks (say, a provider is disabled), count and models.length could diverge, so DATA.count might not equal DATA.models.length. It would be safer to use DATA.models.length as the denominator:
Showing {filtered.length} of {DATA.models.length} models.* docs: address PR #43 review findings on ai models table - Generator now sets an explicit limit and errors on suspected truncation instead of silently returning a partial snapshot. - formatPrice falls back to exponential notation below $0.001 so very cheap models no longer render as the broken string "$". - Footer denominator uses live models.length, not the snapshot-time count. - Drops the Supabase project URL from the snapshot — it is not consumed by the component and there is no reason to embed it in committed JSON. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: drop redundant count field from ai-models snapshot The component reads DATA.models.length now, so count was dead data that could silently drift out of sync on manual JSON edits. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
content/docs/web/models.mdx(search by id/provider/name, chip filters for provider and capability, columns for context length and per-Mtok pricing).pnpm gen:modelsscript (scripts/gen-models-table.ts) writes a snapshot of the Supabaseai_modelscatalog tosrc/data/ai-models.json— 84 available, non-hidden models across 16 providers in the current run.NEXT_PUBLIC_SUPABASE_URL/NEXT_PUBLIC_SUPABASE_ANON_KEYfrom env. The existing Vercel-managed values were already set on Preview/Production; I added the same pair to the Development environment sovercel env pullpopulates.env.local.Why
The previous
models.mdxargued against a static table because the live catalog can drift. Keeping it as a regenerated snapshot — owned by a single script and rendered by one client component — gives docs readers a browseable catalog without claiming it is real-time. The intro now positions the table as "a recent snapshot" with the web app as source of truth.Test plan
pnpm types:checkclean (verified locally)pnpm lintclean (verified locally)pnpm gen:modelsregenerates the snapshot deterministically/web/modelsinpnpm devand confirm: table renders, search filters rows, provider/capability chips toggle, "Clear" resets, dark mode contrast is acceptableNEXT_PUBLIC_SUPABASE_*env vars (no runtime use; the JSON is committed)Notes for reviewers
src/data/ai-models.jsonis committed and intended to be refreshed by hand-runningpnpm gen:models. No build-time fetch.🤖 Generated with Claude Code
Need help on this PR? Tag
@codesmithwith what you need.