Skip to content

LLM/AI Model Evaluation — Quick Start

This guide creates an experiment that compares two models on a customer support task: claude-3-5-sonnet-20241022 is the control and gpt-4o the treatment.

Run the commands in one terminal, in order, against the stack from the Quick Start. Each uses the shell variables set by the ones before it.

Steps 3 to 5 need a provider key. They call the models, so the API needs a key for each provider your variants use. Steps 1, 2 and 6 need no key, and only they are commands to run here.


1. Create an Experiment

Log in first. Creating and starting an experiment takes the DEVELOPER or ADMIN role:

TOKEN=$(curl -s -X POST localhost:8000/api/v1/auth/login \
  -H 'content-type: application/json' \
  -d '{"email":"admin@demo.com","password":"Demo1234!"}' | jq -r .access_token)

curl -s localhost:8000/api/v1/auth/me -H "Authorization: Bearer $TOKEN" | jq .role

It prints "ADMIN".

Create the experiment, with one variant per model. The collection URL ends with a slash, /api/v1/llm-experiments/; without it the API answers 307, which curl doesn't follow. This saves the experiment's id in $EXPERIMENT_ID:

EXPERIMENT=$(curl -s -X POST localhost:8000/api/v1/llm-experiments/ \
  -H "Authorization: Bearer $TOKEN" \
  -H 'content-type: application/json' \
  -d '{
    "name": "Customer Support: Claude vs GPT-4o",
    "description": "Compare Claude Sonnet and GPT-4o on customer support quality",
    "task_type": "chat_completion",
    "evaluation_metric": "business_metric",
    "variants": [
      {
        "name": "control_claude",
        "is_control": true,
        "traffic_split": 0.5,
        "provider": "anthropic",
        "model_name": "claude-3-5-sonnet-20241022",
        "system_prompt": "You are a helpful customer support agent for Acme Corp.",
        "prompt_template": "Customer question: {{customer_message}}\n\nProvide a clear, empathetic response.",
        "temperature": 0.7,
        "max_tokens": 500
      },
      {
        "name": "treatment_gpt4o",
        "is_control": false,
        "traffic_split": 0.5,
        "provider": "openai",
        "model_name": "gpt-4o",
        "system_prompt": "You are a helpful customer support agent for Acme Corp.",
        "prompt_template": "Customer question: {{customer_message}}\n\nProvide a clear, empathetic response.",
        "temperature": 0.7,
        "max_tokens": 500
      }
    ]
  }')
EXPERIMENT_ID=$(jq -r .id <<<"$EXPERIMENT")

jq '{name, status, variants: [.variants[].name]}' <<<"$EXPERIMENT"

The API answers 201 Created:

{
  "name": "Customer Support: Claude vs GPT-4o",
  "status": "DRAFT",
  "variants": [
    "control_claude",
    "treatment_gpt4o"
  ]
}

The response also carries the experiment's id, description, task_type, evaluation_metric, and each variant's full settings and id.


2. Start the Experiment

curl -s -X POST localhost:8000/api/v1/llm-experiments/$EXPERIMENT_ID/start \
  -H "Authorization: Bearer $TOKEN" | jq .status

It prints "ACTIVE".


3. Route User Requests Through the Experiment

For each incoming customer message, call the /complete endpoint. The platform assigns the user to a variant deterministically (the same user always gets the same model), renders the variant's prompt template with input_variables, and calls the model.

Send POST /api/v1/llm-experiments/{id}/complete with the user's user_id and the template's input_variables. The answer names the variant and the model, and carries the model's reply (response), latency_ms, cost_usd, the token counts and an evaluation_id, which step 4 uses.

A google variant calls the Gemini REST API with the key in GEMINI_API_KEY; GEMINI_BASE_URL (default https://generativelanguage.googleapis.com) points it at a proxy or gateway instead. With no GEMINI_API_KEY, a Gemini variant's completion answers 502 {"detail":"LLM provider error: GEMINI_API_KEY is not set"}.


4. Record Business Outcomes

When the customer resolves their issue (converts), submit the business metric for that evaluation with POST /api/v1/llm-experiments/{id}/evaluate: the evaluation_id and a business_metric_value, such as 1.0. A human_rating from your QA team, from 1 to 5, goes in the same request.


5. Run Automated Quality Scoring (Optional)

POST /api/v1/llm-experiments/{id}/judge scores the responses on a quality dimension with LLM-as-judge. It takes a criteria (default helpfulness) and a judge_model (default LLM_DEFAULT_JUDGE_MODEL). The judge is called through the Anthropic SDK, so judge_model names a Claude model. A judge call that fails, or a reply without a usable score, leaves that response unscored: score is null in the judge's response, and the results count it in the variant's n_auto_eval_unscored.


6. Read Results and Declare a Winner

curl -s localhost:8000/api/v1/llm-experiments/$EXPERIMENT_ID/results \
  -H "Authorization: Bearer $TOKEN" \
  | jq '{status, total_evaluations, variants: [.variant_stats[] | {variant_name, n_evaluations}]}'

With no completions yet, every count is 0:

{
  "status": "ACTIVE",
  "total_evaluations": 0,
  "variants": [
    {
      "variant_name": "control_claude",
      "n_evaluations": 0
    },
    {
      "variant_name": "treatment_gpt4o",
      "n_evaluations": 0
    }
  ]
}

With traffic, the full response looks like this:

{
  "experiment_id": "550e8400-e29b-41d4-a716-446655440000",
  "experiment_name": "Customer Support: Claude vs GPT-4o",
  "status": "ACTIVE",
  "evaluation_metric": "business_metric",
  "total_evaluations": 1247,
  "winner_variant_name": "treatment_gpt4o",
  "variant_stats": [
    {
      "variant_name": "control_claude",
      "is_control": true,
      "n_evaluations": 624,
      "mean_latency_ms": 1850.3,
      "mean_cost_usd": 0.000923,
      "mean_business_metric": 0.71,
      "business_metric_ci_lower": 0.674,
      "business_metric_ci_upper": 0.746,
      "p_value": null,
      "effect_size": null
    },
    {
      "variant_name": "treatment_gpt4o",
      "is_control": false,
      "n_evaluations": 623,
      "mean_latency_ms": 1120.6,
      "mean_cost_usd": 0.001245,
      "mean_business_metric": 0.79,
      "business_metric_ci_lower": 0.756,
      "business_metric_ci_upper": 0.824,
      "p_value": 0.003,
      "effect_size": 0.42
    }
  ]
}

Prompt Template Variables

Use {{double_braces}} in your prompt templates:

Template: "Summarize this article for a {{audience}} audience: {{article}}"

Input variables: {
  "audience": "5-year-old",
  "article": "Quantum computing leverages..."
}

Rendered: "Summarize this article for a 5-year-old audience: Quantum computing leverages..."

Missing variables will cause a 400 Bad Request error.


Pausing and Resuming

Pause the experiment:

curl -s -X POST localhost:8000/api/v1/llm-experiments/$EXPERIMENT_ID/pause \
  -H "Authorization: Bearer $TOKEN" | jq .status

It prints "PAUSED". While paused, /complete answers 400. Starting it again resumes it:

curl -s -X POST localhost:8000/api/v1/llm-experiments/$EXPERIMENT_ID/start \
  -H "Authorization: Bearer $TOKEN" | jq .status

It prints "ACTIVE".


Permissions

Action Required Role
Create / Start / Pause experiment DEVELOPER or ADMIN
Add / update variants DEVELOPER or ADMIN
Get completion Any authenticated user (READ)
Submit evaluation DEVELOPER or ADMIN
Get results Any authenticated user (READ)
Run LLM-as-judge DEVELOPER or ADMIN