MedEval Judge — Full-stack Medical AI Model Evaluation System using LLM-as-a-Judge methodology. Allows researchers to benchmark and compare SLMs (Small Language Models) by importing their responses and having a judge LLM score them using a standardized rubric (1–5). Each user has their own private workspace with isolated API keys.
- Monorepo tool: pnpm workspaces
- Node.js version: 24
- Package manager: pnpm
- TypeScript version: 5.9
- Frontend: React + Vite (artifacts/llm-judge) — light theme, green primary
- API framework: Express 5 (artifacts/api-server)
- Database: PostgreSQL + Drizzle ORM
- Validation: Zod (
zod/v4),drizzle-zod - API codegen: Orval (from OpenAPI spec)
- Auth: Dual auth — Replit Auth (OIDC + PKCE) + Firebase Auth (Google + Email/Password); both share cookie-based sessions in PostgreSQL
- Build: esbuild (CJS bundle)
- Charts: Recharts
artifacts/llm-judge— React/Vite frontend (previewPath:/)artifacts/api-server— Express 5 API server (previewPath:/api)lib/db— Drizzle ORM database layerlib/api-spec/openapi.yaml— OpenAPI specification (source of truth)lib/api-client-react— Generated React Query hookslib/api-zod— Generated Zod validation schemas (auth schemas appended manually at bottom)lib/replit-auth-web— Browser auth hook (useAuth()) for React
users— Auth users; Replit users use their Replit ID, Firebase users usefb_<uid>prefix (id, email, firstName, lastName, profileImageUrl)sessions— Server-side sessions for Replit Auth (sid, sess JSONB, expire)models— AI model registrydatasets— Question datasets (Medical, Legal, General domains)questions— Questions with gold answers, supports MCQ and OPEN_ENDED, metadata as JSONBmodel_responses— LLM-generated responses with inference time trackingjudge_evaluations— LLM-as-a-Judge scores (1-5) with Chain-of-Thought reasoning; includesjudge_model_versionandconfirmed_modelaudit columnsjudge_models— Provider rows (OpenAI, Gemini, Claude, DeepSeek); IDs 9–12settings— Per-user key-value store; UNIQUE(user_id, key); FK → users.id
openai_api_key,gemini_api_key,claude_api_key,deepseek_api_key— provider API keysjudge_model_id— int → judge_models.id (which provider is the judge)judge_model_version— free text model name (e.g. "gpt-4o-mini")
- User clicks "Log in with Replit" → redirected to
/api/login?returnTo=<base> - Replit OIDC (PKCE) handles auth → callback to
/api/callback - Session created in
sessionstable, cookiesidset
- User clicks "Continue with Google" or "Sign in with Email" on login page
- Firebase SDK authenticates client-side (popup for Google, inline for email)
- Frontend gets Firebase ID token → POST
/api/auth/firebase-session - Server verifies token via
firebase-admin, upserts user withfb_prefix, creates session cookie onAuthStateChangedlistener inAuthGatedetects user → exchanges token → setsfirebaseSessionOk- Logout:
firebaseSignOut()+ POST/api/auth/firebase-logout→ session cleared
- All protected routes require
req.isAuthenticated()(401 if not) - API keys stored per
user_id→ completely isolated between users - Email uniqueness: if Firebase user shares email with existing Replit account, email stored as null to avoid constraint conflict
AuthGatecomponent (artifacts/llm-judge/src/components/auth-gate.tsx) handles both providers; exposescurrentUnifiedUsermodule variable- Google sign-in uses
signInWithPopup; if run inside an iframe (Replit workspace preview) and popup is blocked, user is guided to open the app in a new tab - Firebase project: medevaljudge — config stored as
VITE_FIREBASE_*env vars - Firebase authorized domains must include the Replit dev domain AND the deployed
.replit.appdomain
- Authentication: Dual auth — Replit Auth + Firebase (Google + Email/Password) with per-user isolated workspaces
- Dynamic Model Lists:
/api/settings/available-models?provider=OpenAIfetches real models from each provider using the user's API key - Data Ingestion: Upload CSV/JSONL files to create question datasets
- LLM-as-a-Judge: Judge any response set using user's configured judge LLM
- MCQ Support: Automatic binary scoring (correct=5, wrong=1)
- Open-ended Evaluation: Rubric-based with must_have/nice_to_have metadata
- Analytics: Model comparison, score distribution, Spearman correlation
- Audit Trail:
judge_model_version+confirmed_modelin evaluations
- 1 = Critical error / wrong answer
- 2 = Weak or incomplete answer
- 3 = Partially correct answer
- 4 = Good answer, close to ideal
- 5 = Excellent answer, matches or exceeds gold standard
Unified interface in artifacts/api-server/src/lib/llm.ts supports:
- OpenAI (gpt-4o, gpt-4o-mini, o1, o3, o4, etc.)
- Google Gemini (gemini-2.0-flash, gemini-1.5-pro, etc.)
- Anthropic Claude (claude-3-5-sonnet, claude-3-5-haiku, etc.)
- DeepSeek (deepseek-chat, deepseek-reasoner, etc.)
callLLM() returns { text, inferenceTimeMs, confirmedModel } — confirmedModel is the model ID echoed back by the provider.
Provider APIs called by the backend using the authenticated user's key:
- OpenAI:
GET https://api.openai.com/v1/models→ filter for gpt-/o1/o3/o4 prefixes - Gemini:
GET https://generativelanguage.googleapis.com/v1beta/models?key=...→ filter generateContent models - Claude:
GET https://api.anthropic.com/v1/models - DeepSeek:
GET https://api.deepseek.com/v1/models
pnpm run typecheck— full typecheck across all packagespnpm run build— typecheck + build all packagespnpm --filter @workspace/api-spec run codegen— regenerate API hooks and Zod schemas from OpenAPI specpnpm --filter @workspace/db run push— push DB schema changes (dev only)pnpm --filter @workspace/api-server run dev— run API server locallypnpm --filter @workspace/llm-judge run dev— run frontend locally
GET /api/auth/user— current auth state ({user: AuthUser | null})GET /api/login— Replit OIDC redirectGET /api/callback— Replit OIDC callbackGET /api/logout— clear session + OIDC end-session redirectPOST /api/auth/firebase-session— exchange Firebase ID token for server session cookiePOST /api/auth/firebase-logout— clear Firebase-originated server session
GET/POST /api/settings/api-keys— per-user API key managementGET/POST /api/settings/judge-model— per-user judge model configGET /api/settings/judge-models— provider list (public)GET /api/settings/available-models?provider=X— live model list from provider API
GET/POST /api/models— model managementGET/PATCH/DELETE /api/models/:idGET/POST /api/datasets— dataset managementGET/DELETE /api/datasets/:idPOST /api/datasets/upload— upload CSV/JSONL questionsGET/POST /api/questions— question managementGET /api/responses— list responsesPOST /api/responses/generate— run inference pipelineGET /api/evaluations— list evaluationsPOST /api/evaluations/run— run LLM-as-a-JudgeGET /api/evaluations/:idGET /api/analytics/summaryGET /api/analytics/model-comparisonGET /api/analytics/score-distributionGET /api/analytics/resultsGET /api/analytics/spearman
The lib/api-spec/package.json codegen script patches lib/api-zod/src/index.ts after codegen to resolve a barrel export conflict. Auth schemas (AuthUser, GetCurrentAuthUserResponse, etc.) are manually appended to the bottom of lib/api-zod/src/generated/api.ts since they don't go through the OpenAPI codegen pipeline.