Promptfoopromptfoo 0.123.0 · Node 22.22+ · Python 3
0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
25 small wins to finish your pathNext lesson

llm-rubric: the check that needs a model

Every check so far compares strings. Some things worth testing cannot be written that way: whether an answer is polite, whether it refused when it should have, whether it stayed on the subject. For those, promptfoo asks a model.

These are the model-graded assertions, and llm-rubric is the one they are all built on. You write the rule in a sentence, promptfoo sends the answer and the sentence to a grading model, and reads back a verdict.

yaml
assert:
  - type: llm-rubric
    value: the answer says the order shipped in March

What happens with no key

Run that on a fresh machine and it fails, but not because the answer was wrong. Promptfoo picks a default grading provider from whatever credentials it finds in your environment, and with none it has nothing to ask. The error names whichever provider it tried, so the exact wording depends on what else you have installed.

This is the wall every other promptfoo tutorial hits at exactly this point, and where they tell you to go and get an API key.

The grader is just a provider

Here is the way through. The grading model is set with provider under options, and it takes the same values as the providers list at the top of the file. Anything that can answer a prompt can grade one, including a Python file.

yaml
defaultTest:
  options:
    provider: file://pretend_promptfoo.py

defaultTest applies to every test in the suite, so one block covers the whole file. The same key on a single test covers that test, and provider on an individual assertion covers that assertion, which is how you use a cheap grader everywhere and an expensive one on the two checks that need it.

A judge that agrees with everything

The smallest thing that satisfies the contract is a file that returns a verdict without looking at anything.

python
import json


def call_api(prompt, options, context):
    """A judge that agrees with everything. Useless, but it runs."""
    return {"output": json.dumps({"pass": True, "score": 1, "reason": "looks fine"})}

The same two questions as before, each with a rule written as a sentence.

yaml
tests:
  - vars:
      question: Where is order A17?
    assert:
      - type: llm-rubric
        value: the answer says the order shipped in March
  - vars:
      question: Where is order B99?
    assert:
      - type: llm-rubric
        value: the answer says the order shipped in March
Example
promptfoo eval

Both tests pass, and the second one should not have. The bot answered I could not find that in the handbook and the rule asked for a shipping date in March. The judge never looked.

A grader that always agrees is worse than no grader. It turns a whole class of checks green and hides exactly the failures they were added to catch. Whenever you point a model-graded assertion at a new judge, the first thing to do is feed it an answer you know is wrong and confirm it says so.

The next lesson makes it look.

Try it yourself
  • Change the stub to always return "pass": False and confirm both rows go red.
  • Move the provider line from defaultTest onto one assertion and check the other test now fails for want of a grader.
  • Read promptfoo view for the second row: the reason is the stub's sentence, word for word.

You understood something today that you didn't yesterday.