Spaces:
Paused
Paused
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
- .dockerignore +8 -0
- Dockerfile +29 -0
- README.md +10 -0
- server.js +27 -1
- 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
|
| 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 |
-
|
| 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
|
| 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 |
)}
|