DBDelve
DBDelve는 Rust 기반 빠른 다중 엔진 데이터베이스 클라이언트, 무료 오픈소스, 텔레메트리 없음
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기메이커가 쓴 소개는 위의 한 줄이 전부입니다. 더 자세한 내용은 저장소 README에서 읽을 수 있습니다. 저장소 README ↗
README 전체를 불러오는 중입니다.
README 전문 보기 ↗Live demo: https://llm-eval-harness-jhyr.onrender.com (free hosting, so the first visit after a quiet spell can take up to a minute to wake up)
Tech: Next.js 16 · React 19 · TypeScript · Python · FastAPI · SQLAlchemy · Anthropic and OpenAI SDKs · Docker · GitHub Actions
Paste a prompt, upload test inputs, pick models, press Run. Every input goes through every model. Another model grades every answer. You get a comparison table showing which model was the most accurate, the fastest and the cheapest, and where each one made mistakes. Change the prompt and run again, and the app tells you: "This change improved 8 cases but broke 3. Here they are."
When you build anything on an LLM API, you write a prompt. Then you tweak it to make it "better". The trap is that you think you improved it, but you have no real way to know. Your change may have fixed 5 cases and quietly broken 10 others. Most people eyeball two or three outputs and hope.
That's like editing code with no tests. This project is the test suite for prompts.
| Feature | Description |
|---|---|
| Quick test | Type one prompt (and optionally one input), pick models, and compare the answers side by side in seconds, each graded with a reason. Nothing to upload. One click turns it into a full evaluation. |
| Multi-model runs | Send every test input to several models at once: Claude (Opus, Sonnet, Haiku, Fable), OpenAI GPT, free Google Gemini, Groq, OpenRouter and local Ollama models, plus offline mock models. |
| LLM-as-a-judge | A separate model scores each output from 1 to 10 and explains why. It can use your reference answers and your own grading criteria. |
| Report card per model | Pass rate, average score, average and p95 latency, model cost, judge cost and error count, with "Most accurate", "Fastest" and "Cheapest" badges. |
| Case-by-case matrix | Rows are test cases and columns are models. Click any cell to read the output, the expected answer and the judge's reasoning. Filter to failures or to cases where models disagree. |
| Regression tracking | Automatically compares a run with the previous one on the same dataset. Shows what broke, what got fixed, what is still failing and what is still passing, side by side. |
| Edit and re-run | One click copies a run's settings into a new run, so you change only the prompt. |
| Datasets | Upload CSV, JSON or JSONL, or paste one input per line. Datasets are saved and reusable. |
| Template variables | {{input}} plus any extra column, for example {{product}}. |
| CSV export | Download every result, with formula-injection protection. |
| Zero-key demo mode | Built-in mock models and a mock judge let you try everything for free and offline. |
| Security hardening | API-key auth, rate limiting, upload limits, judge prompt-injection defences, CSP, a server-side proxy that hides keys, pinned and audited dependencies. See SECURITY.md. |
You Frontend (Next.js) Backend (FastAPI) LLM providers
─── ────────────────── ───────────────── ─────────────
1. paste prompt, ─────────▶ POST /api/datasets ──proxy──▶ parse + validate file
upload CSV save Dataset + TestCases (DB)
2. pick models, ─────────▶ POST /api/runs ──proxy──▶ validate models, cost caps
press Run save Run(status=queued)
start background job ───────────┐
3. watch progress ◀───────── poll GET /api/runs/{id} │
for each case × model (5 in parallel):
render prompt ({{input}}) │
call model ─────────────────────────▶ Claude / GPT
measure latency, tokens, cost ◀──────
ask the judge for a 1–10 score ─────▶ judge model
save Result (DB) ◀──────────────────
status = completed
4. read report ◀───────── summary + matrix
5. edit prompt, ─────────▶ new run on SAME dataset
re-run GET /api/runs/{id}/compare ──▶ match results by (case, model)
6. "improved 8, ◀───────── regression report classify: improved / regressed / ...
broke 3"
flowchart LR
B[Browser<br/>React UI] -->|same-origin /api/*| P[Next.js server<br/>proxy route]
P -->|X-API-Key added server-side| A[FastAPI backend]
A --> DB[(SQLite / Postgres)]
A --> R[Runner<br/>asyncio, semaphore]
R --> PR{Provider registry}
PR --> C[Anthropic SDK<br/>Claude]
PR --> O[OpenAI SDK<br/>GPT]
PR --> M[Mock provider<br/>offline]
R --> J[Judge<br/>LLM-as-a-judge]
J --> PR
A --> G[Regression engine]
Design decisions:
Provider.complete(...). Adding a new provider means writing one file.backend/app/model_catalog.json, so you can change prices or add models without touching code.POST /api/runs returns immediately and the UI polls for progress. Each result is saved as soon as it finishes, so a slow model doesn't block the others.asyncio.Semaphore (default 5) keeps you under provider rate limits.| Layer | Choice | Why |
|---|---|---|
| Frontend | Next.js 16 (App Router), React 19, TypeScript, plain CSS | Industry standard. The server route doubles as a secure proxy. No UI library, so fewer dependencies. Supports light and dark themes. |
| Backend | Python 3.13, FastAPI | Async I/O suits many parallel API calls. Pydantic validation and automatic OpenAPI docs at /docs. |
| Database | SQLAlchemy 2 with SQLite by default | Zero setup. Change DATABASE_URL to use Postgres. |
| LLM SDKs | Official anthropic and openai Python SDKs | They handle retries with backoff, typed errors and timeouts. Official packages are also safer than unofficial wrappers. |
| Tests | pytest with FastAPI TestClient | The whole suite runs offline in under a second, using the mock models. |
| Packaging | Docker and docker-compose | One-command deployment, non-root, read-only containers. |
| CI | GitHub Actions | Tests, type check, build, pip-audit, bandit and npm audit on every push. |
eval_llm_model/
├── README.md ← you are here
├── SECURITY.md ← security controls, review results, deployment checklist
├── Dockerfile ← one-container image for free hosting (website public, API private)
├── render.yaml ← Render Blueprint: one-click free deploy
├── deploy/start.sh ← starts the private API, waits for it, then starts the website
├── docker-compose.yml ← run everything with one command
├── .gitignore ← keeps .env, venvs, node_modules and the DB out of git
├── .github/workflows/ci.yml ← CI: tests, audits, build
├── examples/
│ ├── customer_complaints.csv ← 10 sample complaints with expected summaries (one is a prompt-injection test)
│ └── customer_complaints.jsonl ← same idea in JSONL, with an extra {{product}} column
│
├── backend/
│ ├── requirements.txt ← pinned runtime dependencies
│ ├── requirements-dev.txt ← adds pytest, httpx, pip-audit
│ ├── .env.example ← every setting, documented; copy to .env
│ ├── Dockerfile ← non-root Python image
│ ├── app/
│ │ ├── main.py ← FastAPI app: middleware, CORS, routers, error handler
│ │ ├── config.py ← all settings and limits (env vars)
│ │ ├── db.py ← SQLAlchemy engine and session
│ │ ├── models.py ← tables: Dataset, TestCase, Run, Result
│ │ ├── schemas.py ← Pydantic request and response shapes
│ │ ├── security.py ← API key check, rate limiter, body-size limit, headers, CSV-safe cells
│ │ ├── datasets.py ← safe CSV/JSON/JSONL parsing with limits
│ │ ├── templating.py ← safe {{variable}} substitution (no eval, no Jinja)
│ │ ├── judge.py ← LLM-as-a-judge prompt, JSON schema, score parsing
│ │ ├── runner.py ← runs every case × model in parallel, then judges each output
│ │ ├── regression.py ← per-model summary and run-vs-run comparison
│ │ ├── model_catalog.json ← model IDs, labels and prices (edit freely)
│ │ ├── providers/
│ │ │ ├── base.py ← Provider interface, ModelInfo (pricing), Completion
│ │ │ ├── anthropic_provider.py ← Claude via the official SDK (refusal handling, fallbacks)
│ │ │ ├── openai_provider.py ← GPT, and every OpenAI-compatible API (Gemini, Groq, OpenRouter, Ollama)
│ │ │ ├── mock_provider.py ← deterministic offline models and judge
│ │ │ └── registry.py ← which models are available (depends on which keys exist)
│ │ └── routers/
│ │ ├── playground.py ← POST /api/playground (quick test, nothing saved)
│ │ ├── meta.py ← GET /api/models, GET /api/config
│ │ ├── datasets.py ← upload, list, view and delete datasets
│ │ └── runs.py ← create, list, view, compare, export and delete runs
│ └── tests/
│ ├── conftest.py ← isolated temporary DB, mock-only settings
│ ├── test_api.py ← end-to-end API tests, including security behaviour
│ ├── test_playground.py ← quick-test endpoint
│ ├── test_free_providers.py← Gemini/Groq/OpenRouter/Ollama setup, judge fallback, fake local server
│ └── test_units.py ← templating, parsing, judge prompt and verdict tests
│
└── frontend/
├── package.json / package-lock.json ← pinned dependencies (lockfile has integrity hashes)
├── next.config.ts ← security headers, CSP, standalone build
├── .env.example ← BACKEND_URL, BACKEND_API_KEY (server-side only)
├── Dockerfile ← multi-stage, npm ci --ignore-scripts, runs as `node`
├── lib/api.ts ← typed API client and formatting helpers
├── lib/explain.ts ← glossary and FAQ text shared by Home, Help and "?" buttons
├── app/
│ ├── layout.tsx ← page shell and navigation
│ ├── globals.css ← design tokens (light and dark), components
│ ├── page.tsx ← home: two choices, connected providers, saved evaluations
│ ├── try/page.tsx ← Quick test: one prompt, answers side by side
│ ├── help/page.tsx ← plain-language guide, glossary, FAQ
│ ├── runs/new/page.tsx ← 4-step evaluation form with preview and cost estimate
│ ├── runs/[id]/page.tsx ← results: progress, report card, regression, matrix
│ └── api/[...path]/route.ts← secure proxy to the backend (adds the key, allow-lists paths)
├── public/samples/ ← sample CSV behind the "Use sample data" button
└── components/
├── Help.tsx ← the small "?" explanation button
├── ModelPicker.tsx ← model chips grouped by provider, free badges
├── Nav.tsx ← top navigation
├── SummaryCards.tsx ← per-model report cards with best-at badges
├── ResultsMatrix.tsx ← case × model grid with expandable details
├── RegressionPanel.tsx ← "improved X, broke Y", with before/after outputs
└── StatusBadge.tsx
| Tool | Version | Check with |
|---|---|---|
| Python | 3.11 or newer (3.13 tested) | python3 --version |
| Node.js | 20 or newer (22 LTS recommended) | node --version |
| npm | 10 or newer | npm --version |
cd backend
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements-dev.txt
cp .env.example .env # defaults work as-is, with mock models
uvicorn app.main:app --reload --port 8000
Check that it works: open http://127.0.0.1:8000/health, which should show {"status":"ok"}. Interactive API docs are at http://127.0.0.1:8000/docs.
cd frontend
npm ci --ignore-scripts
cp .env.example .env.local
npm run dev
Open http://localhost:3000. Use localhost, not your network IP address. The dev server listens only on your own machine, so other people on your Wi-Fi can't use your API keys.
examples/customer_complaints.csv.Respond to this: {{input}}.The mock models are deterministic fakes for learning the app. The "careful" mock only summarizes when the prompt asks it to, so prompt edits really change its scores.
Claude and GPT APIs are pay-as-you-go, and neither offers a lasting free tier. The app also supports four providers you can use for free. All four speak the same request format, so they work with no code changes.
| Option | Cost | Credit card | What you get | Get the key |
|---|---|---|---|---|
| Google Gemini (recommended first) | Free tier | No | Gemini 3.8 Flash and 3.5 Flash-Lite; strong enough to be the judge | https://aistudio.google.com/apikey |
| Groq | Free tier | No | Very fast open models: GPT-OSS 120B and 20B | https://console.groq.com/keys |
| OpenRouter | Free models | No | Models whose IDs end in :free. The roster changes and upstream providers are often rate-limited, so treat it as a bonus | https://openrouter.ai/settings/keys |
| Ollama (local) | Free forever, offline | No key at all | Runs models on your own Mac; private, but slower | https://ollama.com/download |
| Anthropic (Claude) | Paid; new accounts may get a small one-time trial credit | Usually needed to keep going | Claude Opus, Sonnet, Haiku | https://console.anthropic.com |
| OpenAI (GPT) | Paid; no signup credits for most new accounts | Yes | GPT-4.1 family | https://platform.openai.com |
Free tiers change often. Check each provider's limits page before relying on them. As of October 2026, GitHub Models no longer takes new users and Groq has retired some older Llama models, so older tutorials may be out of date.
The best free setup is a Gemini key plus a Groq key. That gives you real models from two different companies to compare, and Gemini can act as the judge. It takes about 5 minutes:
Gemini: sign in at https://aistudio.google.com/apikey with a Google account. Click Create API key and copy it.
Groq: sign up at https://console.groq.com, open API Keys, click Create API Key and copy it. It is shown only once.
Put both keys in backend/.env:
GEMINI_API_KEY=AIza...
GROQ_API_KEY=gsk_...
FREE_TIER_CONCURRENCY=2 # stay under free rate limits
Restart the backend. The new models appear in the UI with a green Free tier badge.
With no Claude or OpenAI key, the judge falls back automatically to the first available model in JUDGE_FALLBACK_MODEL, which is GPT-OSS 120B on Groq in this setup. It is fast, and it supports strict JSON output.
Fully offline with Ollama: install it from https://ollama.com/download, then:
ollama pull llama3.2 # about 2 GB
ollama pull qwen3:8b # optional second model, about 5 GB
# in backend/.env
OLLAMA_BASE_URL=http://127.0.0.1:11434/v1
Free-tier rules to know:
FREE_TIER_CONCURRENCY.Create keys at https://console.anthropic.com and https://platform.openai.com.
Set a monthly spend limit in each provider's console first.
Add the keys to backend/.env:
ANTHROPIC_API_KEY=sk-ant-...
OPENAI_API_KEY=sk-...
JUDGE_MODEL=claude-opus-5-5
Restart the backend.
backend/.env, which git ignores. Never put them in code, chat messages, screenshots or the frontend.| ID | Provider | Price per 1M input / output tokens |
|---|---|---|
claude-opus-5-5 | Anthropic | $4.00 / $20.00 |
claude-sonnet-5-5 | Anthropic | $2.00 / $10.00 |
claude-haiku-5-5 | Anthropic | $0.10 / $0.50 |
claude-fable-5-1 | Anthropic | $10.00 / $50.00 |
gpt-4.1 | OpenAI | $2.00 / $8.00 |
gpt-4.1-mini | OpenAI | $0.40 / $1.60 |
gpt-4o-mini | OpenAI | $0.15 / $0.60 |
gemini-3.8-flash, gemini-3.5-flash-lite | Google Gemini | free tier |
openai/gpt-oss-120b, openai/gpt-oss-20b | Groq | free tier |
google/gemma-4-31b-it:free | OpenRouter | free |
llama3.2, qwen3:8b | Ollama (local) | free |
mock-good, mock-sloppy, mock-judge | Offline | fake prices, for demos |
Prices and model names change. Check them against each provider's pricing page and edit
backend/app/model_catalog.json. To add any model a provider offers, add one line to that file and restart. For example, afterollama pull mistraladd{"id": "mistral", "provider": "ollama", ...}. Set"structured_output": falsefor models that don't support JSON-schema responses.
claude-opus-5-5, claude-sonnet-5-5 and claude-fable-5-1, requests opt into server-side refusal fallbacks (fallbacks: "default", beta server-side-fallback-2026-07-01). If a safety classifier declines a request, the API retries it on Anthropic's recommended fallback model in the same call. Set "server_fallback": false in the catalog to turn this off.stop_reason before use. A refusal or a cut-off output is recorded as an error on that result instead of being graded silently.calls = cases × models
run cost ≈ calls × (avg prompt tokens × input price + avg answer tokens × output price) / 1,000,000
judge ≈ calls × (about 600 input tokens + about 100 output tokens, at the judge's prices)
For example, 30 complaints × 3 models is 90 model calls plus 90 judge calls. With short prompts that usually costs a few cents. The run page shows the exact model and judge cost after each run.
Home (/). It explains the app to a first-time visitor in plain language:
It also shows which AI providers are connected and lists your saved evaluations. Running jobs show a live counter such as running 34/90.
Help (/help). A step-by-step first test, how to read results, tips, the full glossary (prompt, input, model, grader, ideal answer, pass mark, dataset, regression, tokens) and an FAQ.
"?" buttons. Small ? buttons next to labels and headings explain each term in place. Every results page opens with a one-sentence verdict, for example "In short: Gemini did best. 9 of 10 answers passed."
Quick test (/try). For a single prompt:
{{input}} for the text the prompt works on, or leave it out and just ask a question.Quick tests are not saved. Use an evaluation when you want history and regression tracking.
Evaluate many inputs (/runs/new). Four numbered steps, with a summary bar showing how many answers the run makes and the estimated cost before you press Run evaluation. Drop a file to upload it, and you see a preview of the first rows straight away. Use sample data loads 10 example complaints in one click.
| Field | Meaning |
|---|---|
| Label | A name for this prompt version, for example "v3: added tone rule". It shows up in comparisons. |
| Prompt | Your task prompt. Put {{input}} where each test case goes. If you leave it out, the input is appended at the end. |
| System prompt | Optional system or instruction message sent with every call. |
| Test inputs | Use a saved dataset (needed for regression tracking), upload a file, or paste one input per line. |
| Models | Up to 6 by default. Each shows its price. |
| Judge model | The model that grades outputs. |
| Pass score | A result passes when its score is at least this value (default 7 of 10) and it has no error. |
| Grading criteria | Tell the judge what "good" means, for example "Exactly one sentence. Mentions the problem and what the customer wants." |
Run page (/runs/{id}).
Each row is one test case. Column names are case-insensitive.
| Column | Required | Aliases | Purpose |
|---|---|---|---|
input | yes | text, prompt, question | The test input inserted into {{input}}. |
expected | no | expected_output, reference, answer, ideal | A reference answer. The judge compares meaning, not exact words. |
| any other column | no | Becomes a template variable, for example {{product}}. Names must be valid identifiers. |
CSV
input,expected
"My order arrived broken. I want a refund.","Customer wants a refund for a broken order."
JSON (a list, or {"cases": [...]}; plain strings are allowed)
[
{"input": "The app crashes at login.", "expected": "App crashes on login."},
"I was charged twice."
]
JSONL (one JSON object per line)
{"input": "Checkout fails with error 502.", "expected": "Card checkout fails.", "product": "website"}
Limits (configurable): 1 MB per file, 500 rows, 20,000 characters per cell, UTF-8 text only.
Tips for good datasets:
expected answers wherever you can. Grading with a reference answer is far more reliable than grading without one.{{input}} is the test case input. {{expected}} is also available, but don't leak it to the model under test unless you mean to.{{column_name}} takes its value from any extra dataset column.{{__import__('os')}} is treated as text. This is covered by a test.Example:
You are a support lead. Summarize the complaint about our {{product}} in one sentence.
Mention the core problem and what the customer wants.
Complaint:
{{input}}
For every output, the judge model receives:
<criteria> your grading criteria (or a sensible default)
<task_prompt> the prompt template you are testing
<test_input> the case input
<expected_output> the reference answer (if the dataset has one)
<candidate_output> what the model produced
The judge must reply with JSON that matches a schema ({"score": int, "reasoning": str}). Claude enforces this with structured outputs (output_config.format) and OpenAI with response_format: json_schema. The score is clamped to the 1–10 range.
| Score | Meaning |
|---|---|
| 10 | Fully correct, follows every instruction |
| 7 | Acceptable, minor issues (default pass mark) |
| 4 | Partly wrong |
| 1 | Wrong, empty, off-task, or the call failed |
Making the judge trustworthy:
claude-opus-5-5.This is the main selling point.
| Status | Rule |
|---|---|
| Regressed (broke) | Passed before, fails now |
| Improved (fixed) | Failed before, passes now |
| Still passing | Passed both times |
| Still failing | Failed both times |
| Skipped | Either run has no grade because the model or judge call failed (an outage, not a prompt problem) |
Good workflow: keep the dataset fixed, change one thing per run, label runs clearly, and don't ship a change that breaks cases you care about.
usage: input or prompt tokens, and output or completion tokens.(input_tokens × input price + output_tokens × output price) / 1,000,000, using model_catalog.json. Judge cost is shown separately, so you can tell what testing costs apart from what the models cost.All backend settings come from environment variables or backend/.env.
| Variable | Default | Description |
|---|---|---|
APP_ENV | development | production hides /docs and warns when there is no API key |
DEMO_MODE | false | true shows a public-demo notice asking visitors not to enter private data |
DATABASE_URL | sqlite:///./data/eval_harness.db | Any SQLAlchemy URL. For Postgres, postgresql+psycopg://user:pass@host/db (install psycopg[binary]) |
ANTHROPIC_API_KEY | empty | Enables Claude models |
OPENAI_API_KEY | empty | Enables GPT models |
ENABLE_MOCK_MODELS | true | Offline demo models |
JUDGE_MODEL | claude-opus-5-5 | Default grader |
JUDGE_FALLBACK_MODEL | gpt-4.1,openai/gpt-oss-120b,gemini-3.8-flash,gemini-3.5-flash-lite,qwen3:8b,mock-judge | Tried in order when the default judge is unavailable |
GEMINI_API_KEY | empty | Enables free Gemini models |
GROQ_API_KEY | empty | Enables free Groq models |
OPENROUTER_API_KEY | empty | Enables OpenRouter free models |
OLLAMA_BASE_URL | empty | For example http://127.0.0.1:11434/v1; enables local Ollama models |
FREE_TIER_CONCURRENCY | 2 | Parallel calls per free provider, shared by all runs |
APP_API_KEY | empty | When set, every /api/* request needs the X-API-Key header |
CORS_ORIGINS | http://localhost:3000 | Comma-separated browser origins allowed to call the API directly |
RATE_LIMIT_PER_MINUTE |
Frontend settings go in frontend/.env.local. They are server-side only and never sent to the browser.
| Variable | Default | Description |
|---|---|---|
BACKEND_URL | http://127.0.0.1:8000 | Where the proxy sends requests |
BACKEND_API_KEY | empty | Must equal the backend's APP_API_KEY |
Base path: /api. When APP_API_KEY is set, send X-API-Key: <key>. Full interactive docs are at http://127.0.0.1:8000/docs in development.
| Method and path | Purpose |
|---|---|
GET /health | Liveness probe (no auth) |
GET /api/models | Models you can run, with prices |
GET /api/config | Non-secret limits and judge options |
POST /api/playground | Quick test: {prompt, input?, expected?, system_prompt?, models[], judge?, judge_model?, judge_criteria?}. Returns each model's answer and grade. Nothing is saved; it has its own rate limit. |
POST /api/datasets | Multipart: file (csv/json/jsonl) or pasted (text), plus optional name |
GET /api/datasets | List datasets with case counts |
GET /api/datasets/{id} | A dataset with all its cases |
DELETE /api/datasets/{id} | Delete a dataset and its runs (409 while one of its runs is in progress) |
POST /api/runs | Start a run (rate limited) |
GET /api/runs?dataset_id= | List runs |
GET /api/runs/{id} | Run detail: summary, results, cases, previous_run_id |
GET /api/runs/{id}/compare?base= | Regression report (base defaults to the previous run) |
GET /api/runs/{id}/export.csv | All results as CSV |
DELETE /api/runs/{id} | Delete a finished run |
Example: start a run from the command line.
curl -X POST http://127.0.0.1:8000/api/datasets \
-H "X-API-Key: $APP_API_KEY" \
-F name=complaints -F file=@examples/customer_complaints.csv
curl -X POST http://127.0.0.1:8000/api/runs \
-H "X-API-Key: $APP_API_KEY" -H "Content-Type: application/json" \
-d '{
"dataset_id": 1,
"label": "v1",
"prompt_template": "Summarize this complaint in one sentence:\n{{input}}",
"models": ["claude-haiku-5-5", "gpt-4.1-mini"],
"judge_criteria": "One sentence; states the problem and what the customer wants.",
"pass_threshold": 7
}'
curl -H "X-API-Key: $APP_API_KEY" http://127.0.0.1:8000/api/runs/1/compare
datasets (id, name, created_at)
test_cases (id, dataset_id → datasets, position, input, expected, variables JSON)
runs (id, dataset_id → datasets, label, prompt_template, system_prompt, models JSON,
judge_model, judge_criteria, pass_threshold, status, error,
total_calls, completed_calls, created_at, finished_at)
results (id, run_id → runs, case_id → test_cases, model, output, error,
latency_ms, input_tokens, output_tokens, cost_usd,
score, passed, judge_reasoning, judge_cost_usd)
Tables are created automatically on startup. Deleting a dataset also deletes its cases, runs and results. If the server restarts during a run, that run is marked failed on the next start, so it never stays "running" forever.
For long-lived production schemas, add Alembic migrations (pip install alembic && alembic init migrations).
cd backend && source .venv/bin/activate
pytest -q # 33 tests, offline, under 2 seconds
pip-audit -r requirements.txt # known-vulnerability scan of dependencies
pip install bandit && bandit -q -r app # static security scan
cd ../frontend
npm run typecheck
npm run build
npm audit
What the tests cover:
The repo includes a root Dockerfile and a render.yaml Blueprint. Together they run the whole app as one free web service:
127.0.0.1 inside the container, so nobody can call it directly.APP_API_KEY).DEMO_MODE=true shows visitors a "public demo, don't enter private data" notice.Steps:
GEMINI_API_KEY, GROQ_API_KEY and OPENROUTER_API_KEY. Paste your free keys. Any of them can be left empty, and the offline demo models always work.https://llm-eval-harness-xxxx.onrender.com.Things to know about the free plan:
/app/data, or use Postgres.main redeploys automatically.cp backend/.env.example backend/.env # add provider keys, set APP_API_KEY
python -c "import secrets; print(secrets.token_urlsafe(32))" # generate a key
echo "APP_API_KEY=<same key>" > .env # compose passes it to the frontend
docker compose up --build -d
Open http://localhost:3000. Only the frontend is published, on 127.0.0.1:3000. The backend is reachable only inside the compose network. Put HTTPS in front with Caddy, nginx, or your cloud's load balancer.
backend/Dockerfile. Set the environment variables in the platform dashboard and use managed Postgres for DATABASE_URL.BACKEND_URL to the backend's private URL if possible, and BACKEND_API_KEY.See SECURITY.md § 5. The essentials are APP_ENV=production, a strong APP_API_KEY, provider spend limits, HTTPS, and a backend that is not exposed publicly.
| Need | Change |
|---|---|
| Many users | Postgres instead of SQLite |
| Many or long runs | Move execute_run to a job queue (Celery, RQ, Arq) and run workers separately |
| Several backend replicas | Redis-based rate limiting, and a queue so that only one worker picks up each run |
| Big datasets, lower cost | Anthropic Message Batches API (50% cheaper, asynchronous) |
The full write-up is in SECURITY.md. In summary:
eval, no pickle, no template engine, and ORM-only SQL.--ignore-scripts, and pip-audit, bandit and npm audit in CI. All three were clean at build time. A critical Next.js advisory was found and fixed by upgrading to 16.4.3.| Symptom | Fix |
|---|---|
| UI says "Backend is unreachable" | Start the backend on port 8000, or set BACKEND_URL in frontend/.env.local. |
| Every request returns 401 | BACKEND_API_KEY in the frontend doesn't match APP_API_KEY in the backend. Restart both after editing. |
| No Claude or GPT models in the list | That provider's key isn't set in backend/.env. Restart the backend after adding it. |
| Free model returns 429 | You hit the free-tier limit. Wait a minute (or until tomorrow for daily limits), use fewer cases, or set FREE_TIER_CONCURRENCY=1. |
| Ollama: "Could not reach the Ollama API" | Start the Ollama app, check OLLAMA_BASE_URL, and ollama pull the model first. |
| OpenRouter free model 404s or 429s | The free roster changes and upstream providers go down. Pick a current :free ID from https://openrouter.ai/models and update the catalog. Avoid the openrouter/free router: it picks a different model each call, which makes run-to-run comparisons meaningless. |
| Gemini returns 503 or times out | Google's free tier is overloaded. Wait and retry, or use gemini-3.5-flash-lite. Affected cases show "Not graded" and are skipped in regression reports. |
| "Anthropic rejected the API key (401)" | Wrong or revoked key. Create a new one in the console. |
| "rate limit hit (429)" | Lower CONCURRENCY, or raise your provider tier. |
| "Too many runs" (429 from the app) | The app's own limiter. Wait a minute or raise RATE_LIMIT_PER_MINUTE. |
| Result shows "Output was cut off" | Raise MAX_OUTPUT_TOKENS. Thinking tokens count toward the limit on Claude models. |
| A model ID returns 404 | The ID in model_catalog.json doesn't exist for your account. Fix or remove it. |
| Run stuck on "failed: server restarted" | The server restarted during the run. Start it again. |
| Judge scores look wrong | Add expected answers and clearer grading criteria, try a stronger judge, and read its explanations. |
Add a model: add a line to backend/app/model_catalog.json:
{"id": "gpt-4.1-nano", "provider": "openai", "label": "GPT-4.1 nano", "input_per_mtok": 0.10, "output_per_mtok": 0.40}
Add a provider (for example Gemini, Mistral or a local Ollama):
backend/app/providers/<name>_provider.py with a class that implements Provider.complete(...) and returns a Completion.providers/registry.py behind its own API-key setting."provider": "<name>".Add deterministic checks next to the judge, such as "is valid JSON", "under 30 words" or "contains a keyword". Compute them in runner._one and combine them with the judge score. Exact checks are cheaper and more reliable than an LLM when they apply.
owner_id on datasets and runs10 |
| Run creations per minute per client IP |
PLAYGROUND_RATE_LIMIT_PER_MINUTE | 60 | Quick-test calls per minute per client IP (one call per model) |
MAX_UPLOAD_BYTES | 1000000 | Maximum dataset file size |
MAX_CASES_PER_DATASET | 500 | Maximum rows |
MAX_FIELD_CHARS | 20000 | Maximum characters per cell |
MAX_PROMPT_CHARS | 20000 | Maximum prompt and system prompt length |
MAX_MODELS_PER_RUN | 6 |
MAX_CALLS_PER_RUN | 1000 | Cases × models |
MAX_OUTPUT_TOKENS | 4096 | max_tokens for models under test (Claude's thinking counts toward this) |
REQUEST_TIMEOUT_SECONDS | 60 | Per API call |
CONCURRENCY | 5 | Parallel model calls per run |
npm ci fails | Use Node 20 or newer. Delete node_modules and try again. |
| "Blocked cross-origin request to /_next/hmr" | You opened the network IP address. Open http://localhost:3000 instead. |