From 458cb8049cbc3535a81e67c96d844591e0eaa0d8 Mon Sep 17 00:00:00 2001 From: Charles Howard <96023061+charlesrhoward@users.noreply.github.com> Date: Tue, 20 Oct 2026 14:04:16 -0400 Subject: [PATCH] chore(scripts): read the models catalog from Neon instead of Supabase The Supabase project is being deleted. gen:models now queries public.ai_models in Neon over DATABASE_URL with pg (numeric columns cast to float8 so the snapshot keeps JSON numbers). --- package.json | 2 + pnpm-lock.yaml | 122 ++++++++++++++++++++++++++++++++++++ scripts/gen-models-table.ts | 73 +++++++++++---------- 3 files changed, 159 insertions(+), 38 deletions(-) diff --git a/package.json b/package.json index 4e10a84..83e6a0e 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9b9dda5..8ef1262 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -96,6 +96,9 @@ importers: '@types/node': specifier: ^26.6.3 version: 26.6.3 + '@types/pg': + specifier: ^8.23.1 + version: 8.23.1 '@types/react': specifier: ^19.3.0 version: 19.3.0 @@ -114,6 +117,9 @@ importers: lint-staged: specifier: ^17.6.0 version: 17.6.0 + pg: + specifier: ^8.23.1 + version: 8.23.1 postcss: specifier: ^8.5.28 version: 8.5.28 @@ -1293,6 +1299,9 @@ packages: '@types/node@26.6.3': resolution: {integrity: sha512-dsqMQQoeTLqu9wynDD00q573mNzso3IdQOAfHRJqLCcmCFPoGo9A1bDpUcv/9tnKpErQWv9uKeGfl37EIS02Yg==} + '@types/pg@8.23.1': + resolution: {integrity: sha512-fKVHpikPdg4GKks3JuLEhvwSyvwzF23hnabPy6DD8ljVbC7+6J5dQzdv4arV6jqq57djnMgs1HKBxX4P8aBI3A==} + '@types/react-dom@19.3.0': resolution: {integrity: sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==} peerDependencies: @@ -3074,6 +3083,40 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + pg-cloudflare@1.4.1: + resolution: {integrity: sha512-6PQbsFWZcp9EmJEwy5cGQ2La+AMWpP46lgbb8X+U/XsHIUweYDNCpeuKck5RxL2MdVFi7krbbEi5nX4Zh7JhrQ==} + + pg-connection-string@2.14.1: + resolution: {integrity: sha512-qR3kGNPBLpCNtz0evbKA0Y/MRFXwSSdT+pTJvYp/bXTcReZbvX1kzF0IyTc1QnxqF7AZbOeBhNL8R5mYQZV/MA==} + + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-pool@3.14.0: + resolution: {integrity: sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==} + peerDependencies: + pg: '>=8.0' + + pg-protocol@1.16.1: + resolution: {integrity: sha512-p9VOFMiHB/ZbJATetbg+99PxssTVSQRnyuPSQ67mN1+1KBOjZaZ83ZQzltnxPhJwSsC3nwVjJ10DVJlerbFzLg==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + + pg@8.23.1: + resolution: {integrity: sha512-aL96AHANtWjPLDOLqnhx+ngp9+UK7ETEU8VJrDCGvsSSi/mGLcWYsS6Herg7lmaBJe4uwrfqsa7gTEFaSizDoQ==} + engines: {node: '>= 16.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + + pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -3101,6 +3144,22 @@ packages: resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==} engines: {node: ^10 || ^12 || >=14} + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-bytea@1.0.1: + resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==} + engines: {node: '>=0.10.0'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -3328,6 +3387,10 @@ packages: space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + stable-hash@0.0.5: resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} @@ -3595,6 +3658,10 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -4692,6 +4759,12 @@ snapshots: dependencies: undici-types: 8.9.0 + '@types/pg@8.23.1': + dependencies: + '@types/node': 26.6.3 + pg-protocol: 1.16.1 + pg-types: 2.2.0 + '@types/react-dom@19.3.0(@types/react@19.3.0)': dependencies: '@types/react': 19.3.0 @@ -6935,6 +7008,41 @@ snapshots: path-parse@1.0.7: {} + pg-cloudflare@1.4.1: + optional: true + + pg-connection-string@2.14.1: {} + + pg-int8@1.0.1: {} + + pg-pool@3.14.0(pg@8.23.1): + dependencies: + pg: 8.23.1 + + pg-protocol@1.16.1: {} + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.1 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + + pg@8.23.1: + dependencies: + pg-connection-string: 2.14.1 + pg-pool: 3.14.0(pg@8.23.1) + pg-protocol: 1.16.1 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.4.1 + + pgpass@1.0.5: + dependencies: + split2: 4.2.0 + picocolors@1.1.1: {} picomatch@2.3.2: {} @@ -6957,6 +7065,16 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postgres-array@2.0.0: {} + + postgres-bytea@1.0.1: {} + + postgres-date@1.0.7: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + prelude-ls@1.2.1: {} prop-types@15.8.1: @@ -7289,6 +7407,8 @@ snapshots: space-separated-tokens@2.0.2: {} + split2@4.2.0: {} + stable-hash@0.0.5: {} stop-iteration-iterator@1.1.0: @@ -7649,6 +7769,8 @@ snapshots: word-wrap@1.2.5: {} + xtend@4.0.2: {} + yallist@3.1.1: {} yaml@2.9.1: {} diff --git a/scripts/gen-models-table.ts b/scripts/gen-models-table.ts index a26340a..643abcd 100644 --- a/scripts/gen-models-table.ts +++ b/scripts/gen-models-table.ts @@ -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 * @@ -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'); -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, + 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, + 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; @@ -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(QUERY, [PAGE_SIZE])).rows; + } finally { + await client.end(); } - 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(