Skip to content

Plug it in

Every plug runs the same free live checks and gives you a Diagnosis. Pick the one that matches how your code calls the model.

OpenAI-compatible clients

client = counterfix.guard(OpenAI())  # OpenAI
client = counterfix.guard(OpenAI(base_url="https://api.groq.com/openai/v1", api_key=key))
client = counterfix.guard(OpenAI(base_url="http://localhost:11434/v1", api_key="ollama"))

response.diagnosis holds the result (or counterfix.get_diagnosis(response) for objects that refuse new attributes). Async clients work the same way.

OpenAI Responses API

OpenAI recommends the Responses API for new projects; a guarded client checks it too:

client = counterfix.guard(OpenAI())
r = client.responses.create(model="gpt-5-mini", instructions="Be brief.", input="...")
r.diagnosis.summary()

responses.create and responses.parse, sync or async, streamed or not, are checked. The call is recorded in chat terms (instructions becomes the system message, max_output_tokens the token limit, text.format the JSON schema, function tools and their outputs become tool calls), so every check, judge and fix() works as for a chat call. A response with status="incomplete" and reason max_output_tokens is F1. Retry mode changes Responses fields (max_output_tokens, text.format). Known limit: with previous_response_id or conversation the earlier turns live on OpenAI's servers, so the record sees only the new input (it is marked metadata["server_state"]), and fix() re-runs through Chat Completions.

Newer OpenAI models refuse max_tokens and a non-default temperature on Chat Completions. Re-runs and judges notice the refusal once and resend with max_completion_tokens (and without temperature), and fixes raise whichever limit field your call used.

Anthropic SDK

from anthropic import Anthropic

client = counterfix.guard(Anthropic())
message = client.messages.create(model="claude-haiku-5-5", max_tokens=1024, messages=[...])

tool_use arguments are checked against each tool's input_schema; thinking blocks are kept as reasoning, so "the model spent its whole budget thinking" is caught as F1. A stop_reason of refusal is reported as a provider filter, not as over-refusal (F3).

Streams

stream=True works with all three (chat, Responses, Anthropic): every chunk reaches you the moment it arrives, and the whole answer is checked when the stream ends (stream.diagnosis). Streams are never retried, because the tokens are already with the user.

LiteLLM and LangChain

litellm.callbacks = [counterfix.litellm_callback()]
llm.invoke("...", config={"callbacks": [counterfix.langchain_callback()]})

Callbacks only observe; the latest result is on handler.last_diagnosis, and every result goes to Config.on_diagnosis.

Any function

@counterfix.watch(model="llama3.2")
def ask(prompt: str) -> str:
    return my_local_model(prompt)

The first argument (or prompt= / messages=) is read as the request, the return value (text, or an OpenAI-shaped response) as the answer. Sync and async both work.

OpenTelemetry GenAI spans

Already instrumented (for example with OpenLLMetry)? Check the spans you have:

provider.add_span_processor(counterfix.otel.SpanChecker())

Telling counterfix what the model was given

with counterfix.context(source=retrieved_text, reference="21 days"):
    response = client.chat.completions.create(...)

source lets the hallucination check tell "the source was missing" from "the source was ignored", and lets fix() try the call with the source in the prompt. reference is a known right answer. It works with every plug above.

Settings

Config is shared by all plugs: mode="retry" (one automatic retry with a known fix), on_diagnosis= (a hook per call, e.g. counterfix.log_to("calls.jsonl")), background judge checks (see In production), and enabled=False to switch it off. counterfix fails open: if it errors, your call still returns normally, and provider errors reach your code unchanged.