CrispStrobe commited on
Commit
b80db95
·
1 Parent(s): caae315

feat: add HF Space Docker deployment with live data API

Browse files

- Dockerfile: builds frontend, fetches fresh JSONs from GitHub on startup, runs server on port 7860
- server.js: GET /api/data + /api/benchmarks routes, SPA catch-all, PORT from env
- App.tsx: async data loading from /api/* with static-import fallback (works on both Vercel and HF)
- README: HF Space front matter

Files changed (5) hide show
  1. .dockerignore +8 -0
  2. Dockerfile +29 -0
  3. README.md +10 -0
  4. server.js +27 -1
  5. src/App.tsx +21 -6
.dockerignore ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ node_modules
2
+ dist
3
+ .vercel
4
+ .env
5
+ .env.*
6
+ *.log
7
+ *.tsbuildinfo
8
+ .DS_Store
Dockerfile ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:22-alpine
2
+
3
+ # wget for fetching fresh data on startup
4
+ RUN apk add --no-cache wget
5
+
6
+ WORKDIR /app
7
+
8
+ # Install dependencies
9
+ COPY package*.json ./
10
+ RUN npm install
11
+
12
+ # Copy source and build the frontend
13
+ COPY . .
14
+ RUN npm run build
15
+
16
+ # HF Spaces require port 7860
17
+ EXPOSE 7860
18
+
19
+ # On startup:
20
+ # 1. Download fresh JSON data from the GitHub repo (catches up since last image build)
21
+ # 2. Start the management server (serves static frontend + /api/* routes)
22
+ CMD ["sh", "-c", "\
23
+ echo 'Fetching latest data from GitHub...' && \
24
+ wget -q -O data/providers.json \
25
+ https://raw.githubusercontent.com/CrispStrobe/LLMProviders/main/data/providers.json && \
26
+ wget -q -O data/benchmarks.json \
27
+ https://raw.githubusercontent.com/CrispStrobe/LLMProviders/main/data/benchmarks.json && \
28
+ echo 'Starting server...' && \
29
+ node server.js"]
README.md CHANGED
@@ -1,3 +1,13 @@
 
 
 
 
 
 
 
 
 
 
1
  # LLM Providers
2
 
3
  **Live: [llmproviders.vercel.app](https://llmproviders.vercel.app)**
 
1
+ ---
2
+ title: LLM Providers
3
+ emoji: 📊
4
+ colorFrom: blue
5
+ colorTo: indigo
6
+ sdk: docker
7
+ app_port: 7860
8
+ pinned: false
9
+ ---
10
+
11
  # LLM Providers
12
 
13
  **Live: [llmproviders.vercel.app](https://llmproviders.vercel.app)**
server.js CHANGED
@@ -16,7 +16,7 @@ const path = require('path');
16
  const fs = require('fs');
17
 
18
  const app = express();
19
- const PORT = 3001;
20
  const DATA_FILE = path.join(__dirname, 'data', 'providers.json');
21
  const BENCHMARKS_FILE = path.join(__dirname, 'data', 'benchmarks.json');
22
  const SCRIPTS_DIR = path.join(__dirname, 'scripts', 'providers');
@@ -45,6 +45,23 @@ if (fs.existsSync(distDir)) {
45
  app.use(express.static(distDir));
46
  }
47
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  // ------------------------------------------------------------------
49
  // GET /api/status
50
  // Returns per-provider: model count, lastUpdated, whether a fetcher script exists
@@ -222,8 +239,17 @@ app.post('/api/fetch', async (req, res) => {
222
  res.json({ results });
223
  });
224
 
 
 
 
 
 
 
 
225
  app.listen(PORT, () => {
226
  console.log(`Management API server running at http://localhost:${PORT}`);
 
 
227
  console.log(` GET /api/status`);
228
  console.log(` POST /api/fetch (all providers)`);
229
  console.log(` POST /api/fetch/:key (single provider)`);
 
16
  const fs = require('fs');
17
 
18
  const app = express();
19
+ const PORT = process.env.PORT || 3001;
20
  const DATA_FILE = path.join(__dirname, 'data', 'providers.json');
21
  const BENCHMARKS_FILE = path.join(__dirname, 'data', 'benchmarks.json');
22
  const SCRIPTS_DIR = path.join(__dirname, 'scripts', 'providers');
 
45
  app.use(express.static(distDir));
46
  }
47
 
48
+ // ------------------------------------------------------------------
49
+ // GET /api/data → serve providers.json live (for HF Space / local)
50
+ // GET /api/benchmarks → serve benchmarks.json live
51
+ // ------------------------------------------------------------------
52
+ app.get('/api/data', (req, res) => {
53
+ try {
54
+ res.json(JSON.parse(fs.readFileSync(DATA_FILE, 'utf8')));
55
+ } catch { res.status(500).json({ error: 'Failed to read providers data' }); }
56
+ });
57
+
58
+ app.get('/api/benchmarks', (req, res) => {
59
+ try {
60
+ if (!fs.existsSync(BENCHMARKS_FILE)) return res.status(404).json([]);
61
+ res.json(JSON.parse(fs.readFileSync(BENCHMARKS_FILE, 'utf8')));
62
+ } catch { res.status(500).json({ error: 'Failed to read benchmarks data' }); }
63
+ });
64
+
65
  // ------------------------------------------------------------------
66
  // GET /api/status
67
  // Returns per-provider: model count, lastUpdated, whether a fetcher script exists
 
239
  res.json({ results });
240
  });
241
 
242
+ // SPA catch-all: serve index.html for any non-API route
243
+ if (fs.existsSync(distDir)) {
244
+ app.get('*', (req, res) => {
245
+ res.sendFile(path.join(distDir, 'index.html'));
246
+ });
247
+ }
248
+
249
  app.listen(PORT, () => {
250
  console.log(`Management API server running at http://localhost:${PORT}`);
251
+ console.log(` GET /api/data`);
252
+ console.log(` GET /api/benchmarks`);
253
  console.log(` GET /api/status`);
254
  console.log(` POST /api/fetch (all providers)`);
255
  console.log(` POST /api/fetch/:key (single provider)`);
src/App.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import { useState, useMemo, useCallback } from 'react'
2
  import providersData from '../data/providers.json'
3
  import benchmarksData from '../data/benchmarks.json'
4
  import { ManagementPanel } from './components/ManagementPanel'
@@ -101,12 +101,27 @@ function App() {
101
  const [showManagement, setShowManagement] = useState(false);
102
  const [dataVersion, setDataVersion] = useState(0);
103
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  // Build benchmark lookup maps
105
  const { nameMap, hfIdMap } = useMemo(() => {
106
  const nameMap = new Map<string, BenchmarkEntry>();
107
  const hfIdMap = new Map<string, BenchmarkEntry>();
108
 
109
- for (const b of benchmarksData as BenchmarkEntry[]) {
110
  // Name-based lookup (LLMStats names + HF model part)
111
  nameMap.set(normalizeName(b.name), b);
112
  if (b.slug) {
@@ -130,7 +145,7 @@ function App() {
130
  if (b.arena_name) nameMap.set(normalizeName(b.arena_name), b);
131
  }
132
  return { nameMap, hfIdMap };
133
- }, []);
134
 
135
  const findBenchmark = useCallback((modelName: string): BenchmarkEntry | undefined => {
136
  // Strip @region (e.g. @us-east-1) and :effort (e.g. :high) suffixes before normalizing
@@ -208,7 +223,7 @@ function App() {
208
  const allModels = useMemo(() => {
209
  const rawModels: (Model & { provider: Provider; complianceStatus: string })[] = []
210
 
211
- providersData.providers.forEach((provider: Provider) => {
212
  const status = getComplianceStatus(provider);
213
  provider.models.forEach((model) => {
214
  let cleanName = model.name;
@@ -236,7 +251,7 @@ function App() {
236
  });
237
 
238
  return uniqueModels;
239
- }, [])
240
 
241
  const filteredModels = useMemo(() => {
242
  return allModels.filter((model) => {
@@ -367,7 +382,7 @@ function App() {
367
  </div>
368
  <div className="header-actions">
369
  {dataVersion > 0 && (
370
- <span className="data-stale-hint" title="Data was refreshed reload the page to see updated prices">
371
  ↻ data updated
372
  </span>
373
  )}
 
1
+ import { useState, useMemo, useCallback, useEffect } from 'react'
2
  import providersData from '../data/providers.json'
3
  import benchmarksData from '../data/benchmarks.json'
4
  import { ManagementPanel } from './components/ManagementPanel'
 
101
  const [showManagement, setShowManagement] = useState(false);
102
  const [dataVersion, setDataVersion] = useState(0);
103
 
104
+ // Live data — initialized from bundled JSON, refreshed from /api/* when available
105
+ const [liveProviders, setLiveProviders] = useState<Provider[]>((providersData as any).providers);
106
+ const [liveBenchmarks, setLiveBenchmarks] = useState<BenchmarkEntry[]>(benchmarksData as BenchmarkEntry[]);
107
+
108
+ useEffect(() => {
109
+ fetch('/api/data')
110
+ .then(r => r.ok ? r.json() : null)
111
+ .then(d => { if (d?.providers) setLiveProviders(d.providers); })
112
+ .catch(() => {});
113
+ fetch('/api/benchmarks')
114
+ .then(r => r.ok ? r.json() : null)
115
+ .then(d => { if (Array.isArray(d)) setLiveBenchmarks(d); })
116
+ .catch(() => {});
117
+ }, [dataVersion]);
118
+
119
  // Build benchmark lookup maps
120
  const { nameMap, hfIdMap } = useMemo(() => {
121
  const nameMap = new Map<string, BenchmarkEntry>();
122
  const hfIdMap = new Map<string, BenchmarkEntry>();
123
 
124
+ for (const b of liveBenchmarks) {
125
  // Name-based lookup (LLMStats names + HF model part)
126
  nameMap.set(normalizeName(b.name), b);
127
  if (b.slug) {
 
145
  if (b.arena_name) nameMap.set(normalizeName(b.arena_name), b);
146
  }
147
  return { nameMap, hfIdMap };
148
+ }, [liveBenchmarks]);
149
 
150
  const findBenchmark = useCallback((modelName: string): BenchmarkEntry | undefined => {
151
  // Strip @region (e.g. @us-east-1) and :effort (e.g. :high) suffixes before normalizing
 
223
  const allModels = useMemo(() => {
224
  const rawModels: (Model & { provider: Provider; complianceStatus: string })[] = []
225
 
226
+ liveProviders.forEach((provider: Provider) => {
227
  const status = getComplianceStatus(provider);
228
  provider.models.forEach((model) => {
229
  let cleanName = model.name;
 
251
  });
252
 
253
  return uniqueModels;
254
+ }, [liveProviders])
255
 
256
  const filteredModels = useMemo(() => {
257
  return allModels.filter((model) => {
 
382
  </div>
383
  <div className="header-actions">
384
  {dataVersion > 0 && (
385
+ <span className="data-stale-hint" title="Data refreshed from server">
386
  ↻ data updated
387
  </span>
388
  )}