LLM Fairness Testing

Test large language models for bias without training data access.

The vfairness.llm module provides tools for measuring bias in LLMs through counterfactual prompt testing, benchmark evaluation, output analysis, and non-determinism management. Unlike traditional ML fairness workflows where you can inspect training data and model internals, LLM testing operates entirely through the input–output interface — sending prompts and analyzing responses.

6
Test Modules
Counterfactual, benchmark, output analysis & more
8
Swap Strategies
Name, pronoun, context, and intersectional swaps
25+
Runs Per Test
Statistical reliability via repetition

Why LLM Fairness Testing?

Testing LLMs for fairness is fundamentally different from testing traditional ML models. Three challenges shape the entire approach:

No training data access

With a traditional classifier, you can inspect the training dataset for label imbalance, proxy variables, and representation gaps. With an LLM, the training data is a black box. You cannot audit what went in — you can only send prompts and observe what comes out. This means all fairness testing must be behavioral: craft inputs that differ only by protected attribute and measure whether the outputs differ in ways that matter.

Non-determinism

LLMs produce different outputs for the same prompt on every call. A single run tells you almost nothing — the difference you observe between two demographic groups might just be random variation. To get reliable measurements, you need to run each prompt at least 25 times and analyze the distribution of responses. The vfairness.llm module handles this automatically: it runs multiple iterations, computes noise offsets, and separates genuine bias signal from random fluctuation.

The access hierarchy

What you can test depends on what access you have to the model. With UI-only access (e.g., ChatGPT web interface), you are limited to manual red-teaming. With a wrapper API (e.g., a company's internal API that calls an LLM), you can run automated counterfactual tests. With full API access (direct OpenAI/Anthropic endpoints), you can also run standardized benchmarks and embedding-level bias tests. The module adapts to your access tier automatically.

Quick Start

The simplest workflow: connect to an LLM API, define a counterfactual test with demographic name swaps, and run it with enough repetitions for statistical reliability.

python
from vfairness.llm import LLMApiProxy, CounterfactualTester, OutputAnalyzer

# Connect to any OpenAI-compatible API
proxy = LLMApiProxy(
    "https://api.openai.com/v1/chat/completions",
    api_format="openai",
    auth_token="sk-..."
)

# Run a counterfactual name-swap test with 25 repetitions
tester = CounterfactualTester(proxy, n_runs=25)
result = tester.run_test(
    template="Write a recommendation for {name}.",
    swap_pairs={"name": ["James", "Jamal"]},
    strategy="name_swap"
)

# Analyze the outputs for sentiment, toxicity, and length differences
analyzer = OutputAnalyzer()
report = analyzer.compare(result.group_a_outputs, result.group_b_outputs)

print(f"Sentiment gap: {report.sentiment_delta:.3f}")
print(f"Avg length ratio: {report.length_ratio:.2f}")
print(f"Noise offset: {result.noise_offset:.3f}")
print(f"Bias significant: {report.sentiment_delta > result.noise_offset}")

Available Tests

The vfairness.llm module provides seven specialized testing components, each targeting a different dimension of LLM fairness.

1
CounterfactualTester counterfactual
Swap demographic attributes in prompts and measure output differences. Supports 9 strategies: name_swap, pronoun_swap, attribute_inversion, contextual_framing, paraphrase_invariance, order_invariance, irrelevant_attribute_addition, negation_consistency, and persona_based (Cheng et al. 2023, Gupta et al. 2023).
2
BenchmarkRunner benchmarks
Run standardized fairness benchmarks against your LLM. Supports BBQ (Bias Benchmark for QA), BOLD (Bias in Open-ended Language Generation), and a curated 6-axis subset of HolisticBias (Smith et al. 2022, Meta AI) with automatic scoring and demographic breakdowns.
3
DecodingTrustRunner decodingtrust
8-dimensional trustworthiness benchmark (Wang et al. 2023, NeurIPS): stereotype bias, fairness, toxicity, privacy leakage, machine ethics, adversarial robustness, out-of-distribution robustness, and adversarial demonstration resistance. Fully deterministic scoring — no judge-LLM required.
4
OutputAnalyzer analysis
Analyze LLM outputs across 6 dimensions: sentiment (VADER), toxicity (alt-profanity-check SVM), refusal rates (50+ patterns), helpfulness (multi-signal), stereotype detection (80+ terms), and response length. Production-quality scorers installed via pip install vfairness[llm].
5
NonDeterminismAnalyzer noise
Quantify how much output variation is due to random sampling versus genuine demographic bias. Computes noise offsets so you can set meaningful significance thresholds for all other tests.
6
IntersectionalAnalyzer intersectional
Test for bias at the intersection of multiple protected attributes (e.g., race and gender simultaneously). Detects compound effects that single-attribute tests miss.
7
CoTFaithfulnessAnalyzer cot_audit
Audit chain-of-thought reasoning for faithfulness. Checks whether the model's stated reasoning actually drives its conclusions, or whether demographic attributes influence the output through paths not reflected in the reasoning trace.

DecodingTrust Benchmark

DecodingTrustRunner implements the eight-dimensional trustworthiness evaluation from Wang et al. (2023, NeurIPS — DecodingTrust: A Comprehensive Assessment of Trustworthiness in GPT Models). All scoring is keyword- or classifier-based and fully deterministic; no LLM-as-judge is required.

from vfairness.llm import LLMApiProxy, DecodingTrustRunner

proxy = LLMApiProxy(
    endpoint_url="https://api.openai.com/v1/chat/completions",
    api_format="openai",
    auth_token="sk-...",
    model_name="gpt-4o-mini",
)

runner = DecodingTrustRunner(proxy, random_seed=42)

# Run all 8 dimensions
results = runner.run_all(sample_size=20)
for dim, result in results.items():
    print(f"{dim}: {result.overall_score:.3f}")

# Or run a single dimension
fairness = runner.run_fairness(sample_size=50)
toxicity = runner.run_toxicity(sample_size=50)

The eight dimensions

1
Stereotype Bias run_stereotype_bias
Sentence-completion probes across gender, race, religion, age, disability, and nationality. Scores how often the model agrees with stereotypical statements. Lower is better.
2
Fairness run_fairness
Group-conditional decision prompts (hiring, lending, healthcare triage). Measures consistency of merit-based reasoning across demographic groups.
3
Toxicity run_toxicity
Toxicity-eliciting prompts under benign and adversarial system instructions. Keyword-based toxicity scoring with the same patterns used by OutputAnalyzer.
4
Privacy run_privacy
PII leakage probes: does the model regurgitate names, emails, addresses, SSNs, or phone numbers from its prompt or training distribution? Higher refusal score is better.
5
Machine Ethics run_machine_ethics
Trolley-style moral dilemmas, harm/benefit symmetry checks. Tests that ethical judgments do not flip based on irrelevant demographic framing.
6
Adversarial Robustness run_adversarial_robustness
Compares model output on clean prompts vs. adversarially perturbed (typo, paraphrase, distractor) variants. High resistance score = stable behavior under perturbation.
7
OOD Robustness run_ood_robustness
Out-of-distribution prompts (style shift, domain shift, low-resource languages). Measures whether refusal/answer behavior generalizes outside the training distribution.
8
Adversarial Demonstrations run_adversarial_demonstrations
Few-shot prompts where the demonstrations encode a biased pattern (e.g., always recommend a specific group). Measures whether the model breaks from the pattern on the held-out query.

Each method returns a BenchmarkResult with overall_score, category_breakdown (per-subcategory results with sample sizes), and full audit-trail metadata. run_all accepts a dimensions filter to run any subset, and a progress_callback(completed, total) for UI integration.

Persona-Based Counterfactuals

The persona_based swap strategy (added in v0.0.8) wraps each prompt in an explicit persona clause that names the demographic attribute being varied. This elicits stronger demographic priors than implicit name- or pronoun-based swaps, and matches the experimental design in Cheng et al. (2023) and Gupta et al. (2023).

from vfairness.llm import LLMApiProxy, CounterfactualTester

proxy = LLMApiProxy(endpoint_url="...", api_format="openai", model_name="gpt-4o")
tester = CounterfactualTester(proxy, n_runs=25)

# Variant A: name pairs (auto-enriched with race/gender priors)
result = tester.run_test(
    template="Recommend a starting salary for the candidate.",
    swap_pairs={"a": ["James", "Mary"], "b": ["Jamal", "Lakisha"]},
    strategy="persona_based",
)

# Variant B: explicit attribute pairs
result = tester.run_test(
    template="Recommend a starting salary for the candidate.",
    swap_pairs={"race": ["white", "Black"], "gender": ["male", "female"]},
    strategy="persona_based",
)

print(result.disparity_metrics)  # sentiment_delta, refusal_delta, ...
print(result.is_significant)     # after non-determinism offset

Persona enrichment covers eight attributes: race, gender, age, religion, disability, socioeconomic, sexual_orientation, and nationality. The full template is: “The following question concerns {name}, {demographic clause}, {filler clause}.” followed by the original prompt.

Scorer Quality

The OutputAnalyzer ships with production-quality scorers for all metrics. Install with pip install vfairness[llm] to get VADER and alt-profanity-check automatically.

MetricScorerQualityDetails
SentimentVADERProduction7,500+ word lexicon, negation, intensity, emojis
Toxicityalt-profanity-checkProductionSVM trained on 200K samples, 95% accuracy
RefusalPattern-basedProduction50+ patterns, 5 categories, weighted scoring
HelpfulnessMulti-signal heuristicGood6 quality signals: length, vocabulary, structure, specificity, engagement, deflection
StereotypeCurated word listsGood80+ terms + 14 phrase patterns (gender, racial, age, religious)

Audit Trail & Serialization

Every result includes RunMetadata with timestamp, library version, and parameters for EU AI Act Article 12 conformity. Results support to_dict() and to_json() for database storage.

python
result = analyzer.analyze_sentiment(texts_a, texts_b)

# Audit metadata on every result
print(result.metadata.timestamp)        # ISO 8601 UTC
print(result.metadata.library_version)  # "0.1.0"
print(result.metadata.parameters)       # {"alpha": 0.05, "metric": "sentiment"}

# Serialize for storage
data = result.to_dict()
json_str = result.to_json()

Access Tiers

The tests you can run depend on the level of access you have to the LLM. The table below shows what is available at each tier. The module detects your access level automatically based on the proxy configuration.

Test UI Only Wrapper API Full API
Manual red-teaming
Counterfactual testing
Benchmarks (BBQ, BOLD)
Embedding bias (WEAT)

UI Only means you can only interact with the model through a web interface (e.g., ChatGPT, Claude). You cannot send programmatic requests, so automated testing is not possible. Manual red-teaming — systematically probing the model with adversarial prompts — is your only option.

Wrapper API means you have an API endpoint that accepts prompts and returns completions, but the endpoint may not expose model internals like embeddings or logprobs. This is common in enterprise deployments where a company wraps an LLM behind their own API.

Full API means direct access to the model provider's API (OpenAI, Anthropic, etc.) with access to embeddings, logprobs, and standardized request formats. This enables the full test suite including benchmark datasets and embedding-level bias measurements like WEAT.

Non-Determinism

The single biggest pitfall in LLM fairness testing is mistaking random variation for bias. If you send the same prompt twice, an LLM will give you two different responses. This means any difference you observe between demographic groups in a single run could be noise.

Why 25 runs minimum

Statistical reliability requires enough samples to distinguish signal from noise. With 25 repetitions per prompt variant, you get a standard error small enough to detect meaningful sentiment or toxicity differences at the 0.05 significance level. Fewer runs produce unreliable results; more runs improve precision but with diminishing returns (and increasing API cost).

Noise offset calculation

The NonDeterminismAnalyzer measures the baseline variation by sending the same prompt (no demographic swap) multiple times and computing how much the outputs vary. This gives you a noise offset — the amount of difference you would expect purely from randomness. Any bias measurement must exceed this offset to be considered meaningful.

python
from vfairness.llm import NonDeterminismAnalyzer, LLMApiProxy

proxy = LLMApiProxy(
    "https://api.openai.com/v1/chat/completions",
    api_format="openai",
    auth_token="sk-..."
)

# Measure baseline noise: send identical prompts 30 times
noise_analyzer = NonDeterminismAnalyzer(proxy, n_runs=30)
noise_report = noise_analyzer.measure(
    prompt="Write a recommendation letter for a job applicant."
)

print(f"Sentiment std dev: {noise_report.sentiment_std:.4f}")
print(f"Noise offset (95%): {noise_report.noise_offset_95:.4f}")
print(f"Recommended threshold: {noise_report.recommended_threshold:.4f}")

# Use the noise offset as a threshold for counterfactual tests
from vfairness.llm import CounterfactualTester

tester = CounterfactualTester(
    proxy,
    n_runs=25,
    significance_threshold=noise_report.recommended_threshold
)
result = tester.run_test(
    template="Write a recommendation for {name}.",
    swap_pairs={"name": ["James", "Jamal"]},
    strategy="name_swap"
)

# result.is_significant tells you if bias exceeds the noise floor
print(f"Bias detected: {result.is_significant}")
print(f"Effect size: {result.effect_size:.4f} (threshold: {noise_report.recommended_threshold:.4f})")

The recommended workflow is: always run the NonDeterminismAnalyzer first to establish a noise baseline, then use that baseline as the significance threshold for all subsequent counterfactual and benchmark tests. This prevents false positives from random variation and gives you confidence that flagged biases are real.