Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,14 @@
"@types/hast": "^3.0.5",
"@types/mdx": "^2.0.14",
"@types/node": "^26.6.3",
"@types/pg": "^8.23.1",
"@types/react": "^19.3.0",
"@types/react-dom": "^19.3.0",
"eslint": "^9.39.4",
"eslint-config-next": "16.3.8",
"husky": "^9.1.7",
"lint-staged": "^17.6.0",
"pg": "^8.23.1",
"postcss": "^8.5.28",
"tailwindcss": "^4.3.3",
"tsx": "^4.23.15",
Expand Down
122 changes: 122 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

73 changes: 35 additions & 38 deletions scripts/gen-models-table.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/**
* Regenerate src/data/ai-models.json from the Supabase ai_models catalog.
* Regenerate src/data/ai-models.json from the Mogplex ai_models catalog in
* Neon. Needs a read-capable DATABASE_URL in .env.local.
*
* pnpm gen:models
*
Expand All @@ -9,36 +10,43 @@

import { mkdir, writeFile } from 'node:fs/promises';
import { dirname, resolve } from 'node:path';
import { Client } from 'pg';

function requireEnv(name: string): string {
const value = process.env[name];
if (!value) {
console.error(`Missing ${name}. Run \`vercel env pull .env.local\` first.`);
console.error(`Missing ${name}. Add it to .env.local first.`);
process.exit(1);
}
return value;
}

const SUPABASE_URL = requireEnv('NEXT_PUBLIC_SUPABASE_URL');
const SUPABASE_ANON_KEY = requireEnv('NEXT_PUBLIC_SUPABASE_ANON_KEY');
const DATABASE_URL = requireEnv('DATABASE_URL');

const OUTPUT = resolve(process.cwd(), 'src/data/ai-models.json');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning: Document the required DATABASE_URL shape (sslmode) and prefer a read-only role

  • new Client({ connectionString: DATABASE_URL }) carries no TLS configuration of its own.
  • node-postgres defaults to ssl: false unless the connection string carries sslmode=… (or PGSSLMODE is set), and Neon "rejects connections that do not use SSL/TLS" — so a DATABASE_URL copied without the query parameter fails with a confusing server-side error instead of the script's own "Missing DATABASE_URL" message.
  • Document the expected value, e.g. postgresql://<user>:<pass>@<neon-host>/<db>?sslmode=require, in the script header comment and/or the PR description.
  • The requireEnv failure message only says "Add it to .env.local first"; make it say where to get the URL and that sslmode is required.
  • The script only needs select on public.ai_models; prefer a dedicated role with GRANT SELECT on that table over handing every contributor a full production credential, since the connection string now carries a real password instead of the old public anon key.
  • No credential exposure found: .env*.local is gitignored so DATABASE_URL stays out of the repo, and pnpm-lock.yaml is updated so pnpm install --frozen-lockfile in CI still resolves.

const SELECT = [
'id',
'provider',
'name',
'context_length',
'capabilities',
'pricing_input',
'pricing_output',
'pricing_cache_read',
'pricing_cache_write',
'is_available',
'is_hidden',
'is_recommended',
'recommendation_bucket',
].join(',');
// numeric columns are cast to float8 so node-pg returns JSON numbers, matching
// the snapshot shape the PostgREST export produced.
const QUERY = `
select
id,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Consider testing the query independently of the live database

  • The SQL is now embedded in the script, so nothing in tests/ covers the filters (is_hidden = false and is_available = true), the ordering, or the column set.
  • A typo in a column name only surfaces when someone runs the script against production.
  • The repo already runs node --test tests/*.test.mjs; a small test asserting the query string contains the expected filters and ordering would help.
  • Alternatively, a dry-run mode that prints the planned output without writing the file keeps the same benefit without a test.

provider,
name,
context_length,
capabilities,
pricing_input::float8 as pricing_input,
pricing_output::float8 as pricing_output,
pricing_cache_read::float8 as pricing_cache_read,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Note the row-count jump before the snapshot is refreshed

  • The committed src/data/ai-models.json is a ~20 KB file, which can hold at most ~80 model entries at the current formatting, while the Neon query returns 177 rows.
  • The catalog cardinality therefore differs substantially between the two sources, consistent with the old PostgREST path being filtered by anon-role RLS.
  • The snapshot is intentionally not refreshed in this PR, so the next pnpm gen:models will grow the rendered table on content/docs/web/models.mdx several times over.
  • Worth a follow-up PR that refreshes the snapshot, eyeballs the page, and confirms the extra rows are genuinely visible to users rather than rows RLS was intentionally hiding.

pricing_cache_write::float8 as pricing_cache_write,
is_available,
is_hidden,
is_recommended,
recommendation_bucket
from public.ai_models
where is_hidden = false and is_available = true
order by provider asc, name asc
limit $1
`;

type Row = {
id: string;
Expand All @@ -59,28 +67,17 @@ type Row = {
const PAGE_SIZE = 10_000;

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');
url.searchParams.set('is_available', 'eq.true');
url.searchParams.set('order', 'provider.asc,name.asc');
url.searchParams.set('limit', String(PAGE_SIZE));

const res = await fetch(url, {
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: `Bearer ${SUPABASE_ANON_KEY}`,
},
});

if (!res.ok) {
throw new Error(`Supabase REST returned ${res.status} ${res.statusText}`);
const client = new Client({ connectionString: DATABASE_URL });
await client.connect();
let rows: Row[];
try {
rows = (await client.query<Row>(QUERY, [PAGE_SIZE])).rows;
} finally {
await client.end();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Consider an explicit ssl option instead of relying on the URL

  • Passing ssl in the config object and sslmode in the connection string interact: per the node-postgres SSL docs, the ssl object is replaced when the URL contains sslcert/sslkey/sslrootcert/sslmode.
  • If you ever add an explicit ssl: { rejectUnauthorized: true } (or Neon's ca.pem for verify-full), decide deliberately which source of truth wins.
  • Note the choice in a comment so a future reader does not assume the in-code option is authoritative.


const rows = (await res.json()) as Row[];

if (rows.length === 0) {
throw new Error('No rows returned — check RLS policies or filters.');
throw new Error('No rows returned — check the filters and DATABASE_URL.');
}
if (rows.length >= PAGE_SIZE) {
throw new Error(
Expand Down
Loading