Skip to content

Scenarios

A scenario states the expected behavior of a model on one task: the variants it asks, and the correct answer of each. It is fixed, so every model is mapped the same way.

Where scenarios come from

Source How
shipped modelscape list names them; modelscape run pressure runs one
spec a YAML file: a prompt with slots, what to vary, and the correct answer (Specs)
builder a notebook widget that writes a spec from one example prompt (Builder)
agent a coding agent writes the spec for you to review (AI agents)
Python a Template, or a task language when the correct answer needs a computation (Concepts)
package another package registers its scenarios by name, as below

Shipped scenarios

Scenario Question Written as
conditioning Do answers on sensitive topics depend on the language, unlike matched controls? spec
injection Does an analyst triage alerts by the evidence, not by instructions an attacker planted? spec
pressure Does an assistant open restricted data under pressure or claimed rank? spec
sources Is a source rated by its track record, not by its name? spec
evidence Does an analyst switch hypotheses when the evidence does? Python

The specs are in modelscape/scenarios/. Open one in the builder with Builder.load("pressure"). evidence is written in Python because its correct answer weighs every report by its reliability, which a spec cannot compute.

modelscape run takes a shipped name, a spec file, or a Python module that defines SCENARIO. To make your own scenario runnable by name, register it in your package:

[project.entry-points."modelscape.scenarios"]
refunds = "my_package.refunds:SCENARIO"

Scorecard

Every run gives one scorecard per model:

Number Meaning Ideal
accuracy correct answers at the baseline, with a 95% interval 1
answered outputs from which an answer could be read 1
spurious change answers that move where the correct answer stays, beyond noise 0
missed change answers that stay where the correct answer moves 0
role per axis: causal if it should change the answer, else invariant
shifts per axis: where answers change beyond noise only on causal axes
threshold per causal numeric axis: where the answer flips, beside where it should the same point

On a nominal axis, a shift compares a value with the baseline, such as role=director. On an ordinal or numeric axis, it compares neighbors, such as pressure 4 → 5. An unreadable answer counts as a wrong one, so read every number with answered.

Spurious and missed change If the correct answer stays but the model's answer changes beyond noise, that is spurious change. If the correct answer changes but the model's answer stays, that is missed change. model answer stays model answer changes beyond noise correct answer stays invariant axis, e.g. pressure correct answer changes causal axis, e.g. amount ✓ robust spurious change ideal 0 missed change ideal 0 ✓ tracks the task

Comparing models

Pass several targets. The first is the reference:

modelscape run pressure --target openai:<a> --target anthropic:<b>

The report then lists where each model differs from the reference beyond the noise of both. The same works for a new release (--target <old> --target <new>) or for drift over time: label a target, as in --target openai:<model>#2026-10, and run it again later next to a new label. Stored outputs are reused.

To choose between models across several scenarios, save a report per scenario and compare them:

modelscape compare runs/*.json --require "invariant_shifts==0" --require "answered>=0.95"

Gating CI

--require turns a run into a test that exits with status 1 when a requirement is missed:

modelscape run pressure --target openai:<model> --require "accuracy>=0.95" --require "invariant_shifts==0"
modelscape check pressure.json --require "invariant_shifts==0"

Requirements bound accuracy, answered, spurious_change, missed_change, invariant_shifts (the number of shifts on invariant axes), or divergences (where a model differs from the reference). Prefix a case group to check only that group, as in sensitive:invariant_shifts==0, and use --gate-target to check only some models.

- run: pip install "modelscape[openai]"
- run: modelscape run pressure --target openai:<model> --require "invariant_shifts==0"
  env:
    OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}

From Python

import asyncio

from modelscape import CallableTarget, Runner
from modelscape.scenarios import load


def always_grant(request):
    return '{"decision": "grant"}'


async def main():
    scenario = load("pressure")
    target = CallableTarget(always_grant, name="always-grant")
    results = await Runner().run(scenario.experiment([target]))
    print(scenario.report(results).summary())


asyncio.run(main())

CallableTarget wraps any function, so you can map your own application or try modelscape without a model. In a notebook, await the run instead of asyncio.run, and show ReportPage(report, results) to explore it. modelscape.gate.check(report, requirements) checks requirements.

Every prompt

scenario.prompts() returns every variant as a pandas DataFrame, with the pandas extra:

from modelscape.scenarios import load

prompts = load("pressure").prompts()
prompts[prompts["changed"] == "requester"]
Column Holds
case the case the variant is made from
one per axis the variant's value on that axis
changed the axes that differ from the baseline, empty at the baseline
system the system prompt
prompt the user message
reference the correct answer
variant the variant's id, as stored with its outputs

Provenance

Every report records the modelscape version, each model's full settings, the model versions the provider reported serving, failures, token use, and when the outputs were made.