Skip to main content

counterfix

PyPI Python CI Docs License: MIT

Find out why an LLM call failed, and prove the fix works.

One line around the client you already use. counterfix names the likely failure type (F1 to F14) on every call. Ask it for a fix and it changes one thing at a time (raises max_tokens, asks for bare JSON, adds the source text, tells the model to check the question's premises, repeats a missed instruction) and re-runs the call. A change counts only if it passes clearly more often than re-running the call unchanged (a Fisher exact test against a plain retry). When none does, counterfix says so and lists what to try next. We call this counterfactual diagnosis.

counterfix works alongside your evaluation and tracing tools (DeepEval, Ragas, Promptfoo, Langfuse, MLflow, ...): they tell you that calls fail and how often; counterfix tells you why one failed and which change verifiably fixes it.

Status: alpha (latest version in the PyPI badge above). Everything below works today: live checks, judge checks, verified fixes, reports, regression tests, profiles, dataset and RAG evaluation, code evaluation, benchmarks, prompt-injection and personal-data checks, token confidence, and agent and tool-call checks. APIs may still change.

Known limits. Verified fixes are common for cut-off answers and broken JSON, rare for invented facts (3 of 14 on our seed set) and none yet for over-refusals or false premises: there counterfix says so and lists what to try instead. For code that gives wrong results, a small model (qwen2.5:3b) got a verified fix on 2 of 8 repeatable failures. For prompt injection, one fix (delimit_untrusted) was verified on 9 of 10 injected probes for gpt-oss-120b, none for qwen2.5:3b. The 20-probe tools benchmark is a smoke test (qwen2.5:3b passes all 20), not a model comparison. Shipped model profiles are small (54 probes; qwen2.5:3b only partly measured). The labels behind our accuracy numbers are reference labels; a human spot check agreed with 33 of 35 of them. See docs/results.md.

Install

pip install counterfix

The core has 4 small dependencies and needs no API key. counterfix is an independent open-source project, not affiliated with any model provider. If your project already has the OpenAI or Anthropic SDK, nothing else is needed; otherwise add it with pip install "counterfix[openai]" or "counterfix[anthropic]".

Quickstart

import counterfix
from openai import OpenAI

client = counterfix.guard(OpenAI())  # 1. wrap your client (same API as before)
r = client.chat.completions.create(model="gpt-4o-mini", messages=[...])
print(r.diagnosis.summary())  # 2. see why it failed (live checks, free)
print(counterfix.fix(r).explain())  # 3. get a fix that was re-run and verified
found: F1 Truncation (95%)
plain retry: passed 0/6
tried raise_max_tokens: passed 0/2 vs retry 0/6, p=1.000 -> no (still: F1 Truncation)
tried raise_max_tokens_more: passed 5/5 vs retry 0/6, p=0.002 -> VERIFIED
fix: Raise max_tokens a lot (long answers, or reasoning that uses the budget)
  change: {'max_tokens': 4096}
13 re-run(s); stopped at the first verified fix

That is the whole loop. Step 2 costs nothing: live checks make no extra calls. Step 3 re-runs the call (here 13 times) and may use a judge model for checks code cannot do; the judge comes from your own key (a free GROQ_API_KEY or GEMINI_API_KEY is enough; run counterfix setup to see what it finds). "Verified" means the change beat asking again on this call, not that it works on every prompt. How the test works: Verified fixes.

Try it with no key at all:

from counterfix import CallRecord, Message

record = CallRecord(
    messages=[Message(role="user", content="Answer in exactly one word: sky colour?")],
    response_text="The sky is usually blue on a clear day.",
)
print(counterfix.diagnose(record, judge=False).explain())
F6 Dropped instruction (90%)
  why: instructions not met: exactly 1 word (has 9); a one-word answer (has 9 words)
  try: Repeat the broken instructions at the end of the prompt as a short checklist. ...

Plug it into what you use

You use One line
OpenAI SDK, or any OpenAI-compatible API (Groq, Gemini, OpenRouter, Ollama, vLLM, ...) client = counterfix.guard(OpenAI(...))
Anthropic SDK client = counterfix.guard(Anthropic())
LiteLLM litellm.callbacks = [counterfix.litellm_callback()]
LangChain llm.invoke(..., config={"callbacks": [counterfix.langchain_callback()]})
Your own function @counterfix.watch(model="llama3.2") on a function that takes a prompt and returns text

The guarded client behaves like the original: sync or async, stream=True, chat.completions.parse(), the Responses API, with blocks and with_options() all work and stay checked, and your type checker sees the client you passed. Two things differ: isinstance(client, OpenAI) is False (pass client.unwrapped where a library insists on the real object), and type checkers do not know response.diagnosis, so typed code can use counterfix.integrations.get_diagnosis(response). Callbacks only observe; they never change a reply. Details: Plug it in.

In production

client = counterfix.guard(
    OpenAI(),
    log="calls.jsonl",  # every call and its diagnosis, as JSON lines
    sample_rate=0.05,  # judge checks on every flagged call + 5% of the rest
    mode="retry",  # re-run a flagged call once with a known fix
)

Judge checks run on a background thread, capped per hour, so calls return as fast as before. Retry mode is off by default; without it counterfix only reports. For anything else, pass a counterfix.Config (the shortcuts above override it).

counterfix diagnose calls.jsonl --judge --html report.html   # a report you can send a teammate
counterfix fix calls.jsonl --judge --out fixes.jsonl          # search verified fixes in bulk
counterfix export-tests fixes.jsonl --out tests/test_prompt_regressions.py

Each verified fix becomes a pytest case that fails if the failure comes back after a prompt edit or a model change; counterfix.testing.assert_ok(response) does the same in your own tests. Guides: In production, Prompt regression tests.

The same checks over a dataset

The checks and fixes above also run over a whole dataset: the misses grouped by cause, and a verified fix searched for each. Install with pip install "counterfix[evals]".

counterfix eval qa.jsonl --model groq:openai/gpt-oss-120b --metric exact_match --judge --fix 5

counterfix works alongside your evaluation tools rather than replacing them. Metrics and benchmarks those tools already provide (BLEU, ROUGE, BERTScore, judge-scored RAG metrics, PII and toxicity, MMLU, GSM8K, HumanEval and the like) still work in 0.3 but are deprecated, with a warning that names the tool to use instead, and are planned for removal in 0.4.

Area What you get Guide
Datasets Exact match, F1, number, choice, JSON, latency; misses by cause; fixes on up to N misses Evaluate a dataset
RAG Retrieval metrics from gold labels, and whether the retriever (F10) or the model (F11) caused each miss RAG evaluation
Code Tests run in a sandbox, failures named (F12), pass@k, fixes such as sending the error back Code evaluation
Prompt injection Canary probes and a judge check (F13), with verified fixes Safety
Agents Tool misuse (F14) from OpenAI, LangGraph, Anthropic or OpenTelemetry runs Agents and tool calls
Speed and confidence Latency, TTFT and tokens/s percentiles; token confidence where logprobs exist Speed, Confidence

More

with counterfix.context(source=..., reference=...) tell the checks what the model was given (RAG) or the right answer
counterfix profile / compare / drift / route measure models, compare an upgrade on your own calls, alert on drift, pick the cheapest safe model per task
counterfix.otel.export(), counterfix.otel.SpanChecker() send diagnoses to OpenTelemetry, or check the GenAI spans you already have
counterfix lint prompts.jsonl catch prompt problems before you send them
counterfix mcp an MCP server so coding agents can lint prompts and check calls

Full docs: https://kansadwala.github.io/CounterFix/ (or build locally with mkdocs serve). Measured accuracy and fix rates: docs/results.md. A Colab-ready notebook on a free model: examples/quickstart.ipynb.

What it checks

Live checks make no extra LLM calls. They catch truncation (including reasoning models that spend the whole budget thinking), broken JSON or schema mismatches, broken tool-call arguments, refusals, empty answers, provider content filters, and broken instructions that code can verify exactly: sentence, word and bullet counts, letter case, reply language (by script), British or American spelling, banned words, "end with a question", "only the number", "one word" and similar (in the spirit of IFEval). On agent runs they also catch calls to tools that were not offered, repeated identical calls and missing expected calls; with a canary planted in untrusted text, they catch a followed injection for free.

Optional auto-retry: a fix that was re-run

from counterfix import Config, guard

client = guard(OpenAI(base_url=GROQ_URL, api_key=key), config=Config(mode="retry"))
r = client.chat.completions.create(
    model="qwen/qwen3.8-27b",
    messages=[{"role": "user", "content": "List the planets of the solar system, one per line."}],
    max_tokens=20,
)
for f in r.diagnosis.retried_from:
    print("before:", f.failure_type.value, f.evidence[0].detail)
for attempt in r.diagnosis.retries:
    print("retry: ", attempt.changes, attempt.outcome)
print("after: ", r.diagnosis.summary())
before: F1 the answer stopped at the max_tokens limit (20)
retry:  {'max_tokens': 1024} fixed
after:  no failure found by 5 checks

You get back the fixed response (all eight planets), and the record of what changed. Retry keeps whichever response is better, records what it tried, and never raises if the retry fails. Add your own fixes per model with RuleStore and FixRule.

Deeper checks with a judge (diagnose mode)

Some failures need meaning to spot: invented facts, false premises, content instructions ("mention the price"), ambiguity, wrong arithmetic. These checks ask a judge model, so they cost extra calls and never run in live mode; counterfix.diagnose() adds them when you want the deeper look (on failures, on a sample of traffic, or in tests). diagnose() takes a response from a guarded client or a CallRecord.

One mistake gets one cause: a wrong calculation is reported as F8, not also as an invented fact (F4), and a cut-off answer as F1, not also as dropped instructions (F6). The findings that were set aside are kept in report.suppressed.

The F8 check never trusts the judge's arithmetic: the judge writes one expression and counterfix evaluates it with a small whitelist-only evaluator. A sampling check (SamplingCheck) re-asks the model and flags answers that change between samples. An optional Jev back-end (counterfix[jev]) plugs into the same judge interface.

Bring your own key

counterfix never ships API keys. Live checks need none: they wrap your own client and make no calls of their own. Judge checks use a judge you choose, in this order:

  1. counterfix.diagnose(response, judge="groq:qwen/qwen3.8-27b")
  2. counterfix.configure(judge=...), once per process. A list of specs makes a voting panel, e.g. ["groq:qwen/qwen3.8-27b", "gemini:gemini-3.5-flash-lite", "ollama:llama3.2"]
  3. the COUNTERFIX_JUDGE environment variable (comma-separated for a panel)
  4. a key it finds: GROQ_API_KEY or GEMINI_API_KEY (both have free tiers), OPENAI_API_KEY, OPENROUTER_API_KEY, or a model in a local Ollama. A different model family from the one being checked is preferred.
  5. otherwise the checked model judges itself, with a SelfJudgeWarning, because judges are lenient with their own mistakes.

See what counterfix finds on your machine (key values are never printed):

counterfix setup          # keys, local models, and the judge that will be used
counterfix setup --test   # also asks the judge one tiny question

Lint prompts before you send them

from counterfix import lint

for issue in lint("Return ONLY JSON with keys name and age for: Priya, 29."):
    print(issue.rule, issue.title, "->", issue.suggestion)

Or from the command line, e.g. in CI:

counterfix lint prompts/*.txt --fail-on-warning
Rule Flags Tends to cause
L1 A constraint buried in the middle of a long prompt F6
L3 A factual question with no "I don't know" option F4, F5
L4 JSON/YAML/CSV asked for in words only, no example or JSON mode F2
L7 Pasted outside text with no delimiters prompt injection
L2 Instructions that conflict (needs --judge) F6, F7
L5 System prompt and user message disagree (needs --judge) F6, F3
L6 Request missing a key detail (needs --judge) F9

Design rules:

  • Fail open. If counterfix itself errors, your call still returns normally.
  • Your errors stay yours. Exceptions from the provider reach your code unchanged.
  • Safe defaults. Live mode only flags failures and makes no extra LLM calls.
  • Light core. The core depends on 4 small libraries; providers and heavy tools are extras.

Failure types

Code Failure
F1 Truncation
F2 Format break (JSON, schema)
F3 Over-refusal
F4 Hallucination from missing knowledge
F5 False-premise acceptance
F6 Dropped instruction
F7 Prompt fragility
F8 Computation or token-level error
F9 Ambiguous request
F10 Retrieval miss (RAG: the answer was not in the retrieved text)
F11 Context ignored (RAG: the answer was there, the model missed it)
F12 Code fails its tests
F13 Followed instructions planted in retrieved text or tool output (prompt injection)
F14 Tool misuse (wrong, unknown or missing tool call, loop, agent task not done)

API errors, rate limits and provider content filters are tracked separately, so they are never counted as the model's fault.

Development

git clone https://github.com/KANSADWALA/CounterFix.git
cd CounterFix
pip install -e ".[dev]"
pytest

Optional extras: openai, anthropic, litellm, langchain, llamaindex, otel, semantic, metrics, eval, code, benchmarks, safety, mcp, cache, cli, report, jev.

python bench/dev_walkthrough.py SPEC uses the library as a developer would against a real model (about 40 small calls) and prints OK or the error for each step.

Roadmap

Done:

  • guard() for any OpenAI-compatible client, live checks, optional auto-retry ✅
  • Prompt linter (counterfix lint) ✅
  • Judge checks, diagnose(), bring your own key, counterfix setup ✅
  • Reports: log_to(), counterfix diagnose LOG with terminal, JSON and HTML output ✅
  • Background judge checks on a sample of traffic, with a cost cap ✅
  • @counterfix.watch decorator for any function, sync or async ✅
  • Anthropic SDK in guard(), LiteLLM and LangChain callbacks ✅
  • Verified fixes: counterfix.fix() (counterfactual re-runs; a change must beat a plain retry) ✅
  • Prompt regression tests: counterfix export-tests and a pytest plugin ✅
  • Model profiles, compare, drift, route; fixes ordered by what worked per model ✅
  • OpenTelemetry export and GenAI span reader; streams; RAG context; MCP server ✅
  • Released on PyPI (pip install counterfix), docs site, releases by trusted publishing ✅
  • Evaluation, wave 1: counterfix eval, reference metrics, TTFT and speed/cost, verified speed fixes ✅
  • RAG evaluation: retrieval and judge metrics, F10 retrieval miss vs F11 context ignored, verified RAG fixes ✅
  • OpenAI Responses API in guard(); evaluate your own app or RAG pipeline (--app) ✅
  • Code evaluation: sandboxed tests, F12 with its causes, verified code fixes, pass@k ✅
  • Benchmarks: MMLU, GSM8K, ARC, TruthfulQA, IFEval, HumanEval(+), MBPP(+), checked against lm-eval ✅
  • Safety: prompt injection (F13, probes and judge check, verified fixes), PII, toxicity; token confidence and perplexity ✅
  • Agents and tool calls: trajectories from any framework, F14 checks, tool_call metric, verified tool-choice fixes ✅
  • Trust in the metrics: a labelled set and measured accuracy for every judge-based check, and a human spot check of the labels ✅

Next:

  1. A public dataset of diagnosed failures with their verified fixes
  2. Larger labelled sets and full profiles for more models
  3. A BFCL loader, so the tool-use benchmark can compare models

License

MIT

Metadata

Release files for counterfix 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for counterfix 0.3.0
File Size Uploaded Provenance Actions
counterfix-0.3.0.tar.gz 223.0 kB With provenance Details for counterfix-0.3.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for counterfix 0.3.0
File Interpreter ABI Platform Actions
counterfix-0.3.0-py3-none-any.whl Python 3 none any Details for counterfix-0.3.0-py3-none-any.whl

Total release size: 497.6 kB

Release files / counterfix-0.3.0.tar.gz

Download URL counterfix-0.3.0.tar.gz
Size 223.0 kB
Tags Source
SHA-256 checksum
How to use checksums
46b1684777cbb8b313db207925eb186ea794ff563220c55082675bf280fd8e4b
BLAKE2b-256 checksum
How to use checksums
dec416562b0ff8b14ccbecc88a849fd516571b1f3ba9f11c1a9c1d009b543b84
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 12, 2026.

Transparency log

Release files / counterfix-0.3.0-py3-none-any.whl

Download URL counterfix-0.3.0-py3-none-any.whl
Size 274.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
12f2899c1c7362fef86c97fa3a485266e917d2bec24fc7045c7bebd109c67fec
BLAKE2b-256 checksum
How to use checksums
a06f7d7d959216d738182bc7ed71baa431f2dec5a4d2d818f490696c215535e8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 12, 2026.

Transparency log

Security for counterfix 0.3.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

Both files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for counterfix-0.3.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 12, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page