Self-hosted LangSmith/Helicone alternative: trace every LLM call, version prompts and
datasets, run RAG/LLM-judge evals against any provider, and block regressions in CI β
one docker compose up, no vendor lock-in.
Walkthrough of the live free-tier deployment from a freshly registered account (sign-up, dashboard, traces, datasets, prompts, eval runs, analytics) β captured 2026-09-13.
Hosted on Render's free tier, so an idle backend has to cold-start: on 2026-09-13 the first /health request took between 1.5 and 4 minutes to answer. Later requests are fast.
There's no shared demo login β sign up for your own account from the app; it's a real registration against a live Postgres instance, not a mock.
| Overview dashboard | Trace explorer |
|---|---|
![]() |
![]() |
| Eval runs (12 of the 24 built-in metrics are selectable in this form) |
|---|
![]() |
ββββββββββββββββββ
Your app βββSDKββββΆβ FastAPI API βββββββββ
(or OTLP) ββtraceββββΆβ (backend/) β β
βββββββββ¬βββββββββ β
β βΌ
ββββββββΌβββββββ βββββββββββββββ
β PostgreSQL β β Redis β
β traces, β β job queue / β
β datasets, β β cache β
β evals β ββββββββ¬βββββββ
βββββββββββββββ β
ββββββββΌββββββββ
β Celery worker β
β (evaluators: β
β faithfulness, β
β relevance, β
β hallucination,β
β exact/F1) β
β via LiteLLM β
βββββββββββββββββ
β²
ββββββββ΄βββββββββ
β Next.js UI β
β (frontend/) β
βββββββββββββββββ
cd infra
cp .env.example .env # fill in JWT_SECRET; leave provider keys empty to stay local-only
docker compose up --build- API: http://localhost:8000 (docs at
/docs) - UI: http://localhost:3000
Run migrations (done automatically by the backend container on startup, or manually):
docker compose exec backend alembic upgrade headRegister a user and mint an API key:
curl -X POST localhost:8000/api/auth/register -d '{"email":"you@example.com","password":"pw"}' -H 'Content-Type: application/json'
# -> {"access_token": "..."}
curl -X POST localhost:8000/api/auth/api-keys -H "Authorization: Bearer <access_token>" \
-d '{"name":"local-dev"}' -H 'Content-Type: application/json'
# -> {"key": "oe_...", ...} save this, it's shown oncefrom sdk.client import OpenEvalClient
client = OpenEvalClient(api_key="oe_...", base_url="http://localhost:8000")
response = client.completion(
model="ollama/llama3", # any LiteLLM-supported model: openai/gpt-4o, anthropic/claude-..., ollama/llama3, gemini/...
messages=[{"role": "user", "content": "Hello!"}],
tags={"env": "dev", "feature": "chat"},
)Every call is auto-logged: prompt, response, latency, token counts, cost, model, tags β visible immediately in the Traces dashboard.
- Upload a dataset (CSV or JSONL with
input/expected_output/contextcolumns β seeevals/sample_qa.jsonl). For RAG metrics,contextcan hold multiple retrieved chunks by separating them with a\n---\nline; otherwise the whole field is treated as one chunk.curl -X POST "localhost:8000/api/datasets/upload?name=sample-qa" \ -H "Authorization: Bearer oe_..." -F "file=@evals/sample_qa.jsonl"
- Trigger a run:
curl -X POST localhost:8000/api/evals -H "Authorization: Bearer oe_..." -H 'Content-Type: application/json' -d '{ "dataset_id": "<dataset-id>", "target_model": "ollama/llama3", "metrics": ["exact_match", "f1", "answer_relevance", "faithfulness", "hallucination"] }'
- Watch progress in the Eval Runs dashboard, or poll
GET /api/evals/{id}.
Compare two runs and detect regressions:
curl -X POST localhost:8000/api/evals/compare -H "Authorization: Bearer oe_..." -d '{"run_ids": ["<baseline>", "<candidate>"]}'# LangChain and LangGraph (LangGraph runs on LangChain's callback system, so this
# covers both - pass the handler as a callback anywhere a chain/graph accepts one)
from sdk.client import OpenEvalClient
from sdk.integrations.langchain import OpenEvalCallbackHandler
handler = OpenEvalCallbackHandler(OpenEvalClient(api_key="oe_..."), tags={"env": "prod"})
llm.invoke("hello", config={"callbacks": [handler]})# Already have an openai.OpenAI() client and don't want to change call sites:
from sdk.integrations.openai import patch_openai_client
patch_openai_client(my_openai_client, OpenEvalClient(api_key="oe_..."))pip install openeval-sdk[langchain] or [openai] for the optional extras.
Don't want to touch app code at all? Point any OpenAI-compatible client at the bundled LiteLLM proxy instead of the real provider, and every call is traced automatically:
cd infra
docker compose --profile proxy up -d litellm-proxy # off by default, opt-in via --profileimport openai
client = openai.OpenAI(base_url="http://localhost:4000/v1", api_key="anything")
client.chat.completions.create(model="gpt-4o-mini", messages=[{"role": "user", "content": "hi"}])
# ^ traced to OpenEval with zero code changes beyond the base_urlRequires OPENEVAL_API_KEY (and your real provider key) set in infra/.env; see
infra/litellm-proxy/.
- Experiments:
POST /api/experimentsgroups eval runs with a pinned baseline;GET /api/experiments/{id}/comparereturns metric deltas, per-row diffs, and Welch's t-test significance per metric vs. the baseline. - Webhooks:
POST /api/webhooksregisters a URL foreval.completed,eval.regression_detected, oreval.passed(HMAC-signed viaX-OpenEval-Signatureif you set asecret). Fired automatically when an eval run finishes or a compare call detects a regression. - Live progress:
GET /api/evals/{id}/statusis a Server-Sent Events stream of row-by-row eval progress (completed_rows/failed_rows/total_rows), shown as a progress bar in the eval run detail page. - Analytics:
GET /api/analytics/cost,/latency,/usageβ cost by model/day with a naive monthly projection, p50/p95/p99 latency by model, usage by tag. - Prompt playground & promotion:
POST /api/prompts/{version_id}/playgroundrenders a prompt version against any model without saving anything;POST /api/prompts/{version_id}/promoteatomically marks one versionproductionand demotes the previous production version tostaging. - Rate limiting: every authenticated request is checked against a Redis sliding-window
limit (
rate_limit_per_minutein.env, default 120/min per user). - Organizations/Projects/RBAC:
POST /api/organizations(creator becomesowner), invite members with a role (owner/admin/member/viewer), create projects under an org.api/rbac.py:require_role(...)is a reusable dependency for project-scoped routes. Each project also has a monthly trace/eval-run quota (trace_quota_per_month/eval_run_quota_per_month, defaults 1M / 1K);api/rbac.py:check_quota(...)counts rows created since the 1st of the current UTC month and returns 429 once a project hits its limit β enforced on trace ingestion and eval-run creation. - Human annotation:
POST /api/annotations/assignqueues a trace for a reviewer,POST /api/annotations/queue/{id}/submitrecords their scores,POST /api/annotations/kappacomputes Cohen's kappa between two annotators on a criterion,POST /api/annotations/exportturns annotations into a new dataset. - More metrics:
semantic_similarity(local sentence-transformers embeddings, no API calls),json_validity,regex_match,bleu,rouge_lβ all deterministic/local, on top of the original 5. - RAGAS/DeepEval-backed metrics:
faithfulness,answer_relevance, andhallucinationare now backed by realdeepevalmetric implementations (FaithfulnessMetric,AnswerRelevancyMetric,HallucinationMetric) instead of hand-rolled prompts, plus four new metrics:context_precision/context_recall(DeepEval'sContextualPrecisionMetric/ContextualRecallMetricβ the same algorithms RAGAS implements), andcontext_entity_recall/noise_robustness(RAGAS-only metrics with no DeepEval equivalent, implemented as DeepEvalGEvalrubrics matching RAGAS's published definitions) β plustoxicity,coherence,concisenessLLM-as-judge metrics (ToxicityMetric/GEval). All run againstjudge_modelvia a smallevaluators/deepeval_llm.pyadapter (LiteLLMDeepEvalModel), so any litellm-supported provider works, not just OpenAI. Realragasitself could not be installed:ragas0.4.x hard-importslangchain_community.chat_models.vertexai, a module removed whenlangchain-communityhit 0.4 (a legacy langchain-0.3-era dependency chain), while this project'slitellm/instructorneedopenai>=2.20β no combination of package versions satisfies both, sodeepeval(which has no such conflict and covers most of the same ground) is used instead. - Trajectory metrics: score the sequence of steps an agent took, not just its final
answer β
trajectory_task_completion,trajectory_tool_selection(tool-choice F1, with forbidden tools halving the score),trajectory_step_efficiency,trajectory_error_recovery,trajectory_budget_adherence,trajectory_loop_detection(all deterministic/local), plustrajectory_reasoning(GEval judge). They use the ordinary evaluator interface:outputis the agent's recorded run as JSON (steps,terminal_state, token/cost/time totals) andexpected_outputis the task spec as JSON (expected_tools,optimal_steps,budget,success_assertions, ...) β the same field-reinterpretationregex_matchalready uses, so no schema change was needed. Seeevaluators/trajectory.pyfor both shapes. - Synthetic dataset generation:
POST /api/datasets/{id}/generateuses the dataset's own rows as seeds and an LLM to producevariation(realistic paraphrases) oradversarial(edge cases / prompt injection) rows as a new dataset version. - LangChain / LangGraph / OpenAI client integrations: see "LangChain / LangGraph / raw OpenAI client integrations" above.
- Zero-code tracing via LiteLLM proxy: see "Zero-code tracing" above.
- Self-monitoring: backend exposes
GET /metrics(Prometheus format) viaprometheus-fastapi-instrumentatorβ request latency/count by route, plus custom counters incore/metrics.py(openeval_traces_ingested_total,openeval_llm_cost_usd_total,openeval_eval_jobs_total).docker compose --profile monitoring upbrings up Prometheus + Grafana (pre-provisioned dashboards ininfra/grafana/dashboards/) + Flower (Celery task monitoring UI at :5555).
.github/actions/run-eval is a composite GitHub Action that triggers an eval run against a
pinned dataset, polls until it finishes, and comments the results on the PR. See
.github/workflows/eval-on-pr.yml for wiring; set repo vars OPENEVAL_API_URL,
OPENEVAL_DATASET_ID, OPENEVAL_TARGET_MODEL and secret OPENEVAL_API_KEY.
The default judge model is ollama/llama3 (local, free) so a fresh install never calls a
paid API. Point target_model / judge_model at openai/..., anthropic/..., etc. only
when you've supplied your own provider key in infra/.env.
See top of this repo for backend/ (FastAPI + Celery + SDK), frontend/ (Next.js),
infra/ (docker-compose, k8s starting points, Prometheus scrape config), evals/
(example datasets), .github/ (CI + PR eval action).
- LiteLLM everywhere a model gets called (evals, SDK tracing, zero-code proxy) instead of
an OpenAI-only client. Provider is a config string, not a code path β swapping
openai/gpt-4oforollama/llama3oranthropic/claude-*needs no code change, which is the whole point of an eval platform not locking you into one vendor. judge_modeldefaults to a local Ollama model, not GPT-4. An eval platform that quietly bills your OpenAI account ondocker compose upis a bad first impression. Point it at a paid provider explicitly once you've supplied your own key.- DeepEval instead of RAGAS for RAG/LLM-judge metrics, even though RAGAS was the original
target: real
ragas0.4.x hard-importslangchain_community.chat_models.vertexai, a module removed whenlangchain-communityhit 0.4, while this project'slitellm/instructorneedopenai>=2.20β no combination of package versions resolves both. Rather than vendor a patched fork or freeze the rest of the stack to a legacy langchain, DeepEval covers the same ground (including RAGAS-equivalent contextual precision/recall) with no such conflict. The two metrics with no DeepEval equivalent (context_entity_recall,noise_robustness) are implemented as GEval rubrics matching RAGAS's published definitions instead of skipping them. - Celery for eval runs, not a background
asyncio.Task. Eval jobs can run hundreds of rows against a real LLM API and take minutes; that needs to survive an API process restart and be independently scalable (seeinfra/k8s/worker/hpa.yaml), which a request-scoped async task doesn't give you. - SSE for eval progress, not WebSockets. Progress is one-directional (server β client) and HTTP-cacheable/proxy-friendly; a full-duplex socket buys nothing here for real cost.
- Every resource scoped to a project from the start (
api/rbac.py:check_project_role) rather than bolted onto a single-tenant schema later β multi-tenancy retrofits are where authorization bugs live, so traces/datasets/prompts/evals were designed against organization β project β role from the first migration that needed them.
Built and working: ingestion (SDK + LangChain/LangGraph + OpenAI-client-patch + LiteLLM-proxy
zero-code tracing + minimal OTLP/HTTP JSON endpoint), dataset upload/versioning/synthetic
generation, eval engine (24 built-in metrics, several backed by real RAGAS-equivalent/DeepEval
implementations, + custom-metric hook), prompt versioning +
playground + promotion, experiments with significance testing, webhooks, cost/latency
analytics, human annotation queue with Cohen's kappa, organizations/projects/RBAC (every
resource β traces, datasets, prompts, eval runs, experiments, annotations β is scoped to a
project and every route checks the caller's role via require_role/check_project_role),
Redis rate limiting, SSE eval progress, Prometheus self-monitoring (including the Celery
worker's own counters, via PROMETHEUS_MULTIPROC_DIR multiprocess mode β see
core/metrics.py/main.py) + Grafana dashboards, JWT + scoped (read/write/admin) API key
auth, trace feedback (thumbs up/down + comment), bulk trace-to-dataset export, and a
server-side-filterable trace list (model/full-text search/error/latency/cost/date range, not
just whatever the current page happens to contain).
Frontend (Next.js, frontend/app/): login/register, an overview dashboard (traces
today/week, error rate, cost trend, top models, recent runs), a searchable/filterable trace
explorer with inline feedback, dataset management (create/upload/synthetic generation/row
viewer), prompt management (versioning, promote-to-production, playground, unified diff
between versions), eval run creation + live SSE progress, experiment comparison (metric
deltas, significance markers, per-row regressions), an annotation queue (assignee/annotator
pickers backed by GET /organizations/{id}/members, not raw user IDs) + Cohen's kappa
calculator, cost/latency analytics charts, and a Settings page to create/revoke scoped API
keys.
Stubbed as a starting point only (not production-hardened): infra/k8s/*.yaml (secrets
documented in infra/k8s/secret.example.yaml as a template to fill in and apply yourself, or
better, generate via a real secrets manager β Sealed Secrets / External Secrets Operator /
SOPS β rather than kubectl apply of plaintext stringData; the worker HPA needs KEDA
installed in-cluster β see the comment in infra/k8s/worker/hpa.yaml), and the prompt diff
viewer/synthetic-data UI use small dependency-free implementations (a line-diff and manual
JSON forms) rather than Monaco/react-diff-viewer from the original spec β swap in later if
richer editing is worth the added JS payload.
cd backend
pytest # unit tests for evaluators, stats, eval_service β mocked judge calls, no API cost


