diff --git a/.github/workflows/issue-formatter.yml b/.github/workflows/issue-formatter.yml index e7ebf98..ea2c9a9 100644 --- a/.github/workflows/issue-formatter.yml +++ b/.github/workflows/issue-formatter.yml @@ -27,7 +27,7 @@ jobs: 'Authorization': `Bearer ${process.env.OPENAI_API_KEY}` }, body: JSON.stringify({ - model: 'gpt-4o-mini', + model: 'gpt-5.6-luna', messages: [{ role: 'user', content: `Rewrite this GitHub issue body using the exact format below. Keep it short, crisp and easy to understand. Do NOT add any information that wasn't in the original. Output ONLY the formatted markdown, nothing else. @@ -79,7 +79,7 @@ jobs: 'Authorization': `Bearer ${process.env.OPENAI_API_KEY}` }, body: JSON.stringify({ - model: 'gpt-4o-mini', + model: 'gpt-5.6-sol', messages: [{ role: 'user', content: `Based on the issue body below, generate a short title in this exact format: diff --git a/backend/app/core/config.py b/backend/app/core/config.py index 97e9d1e..709d1f3 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -60,8 +60,8 @@ class Settings(BaseSettings): ALLOWED_IPS: Annotated[list[str] | str, BeforeValidator(parse_ip_list)] = [] CORE_DIR: ClassVar[Path] = Path(__file__).resolve().parent OPENAI_API_KEY: str | None = None - ANSWER_RELEVANCE_LLM_MODEL: str = "gpt-4o-mini" - DEFAULT_LLM_CALLABLE: str = "gpt-4o-mini" + ANSWER_RELEVANCE_LLM_MODEL: str = "gpt-5.6-sol" + DEFAULT_LLM_CALLABLE: str = "gpt-5.6-sol" TOPIC_RELEVANCE_LLM_THRESHOLD: int = 2 SLUR_LIST_FILENAME: ClassVar[str] = "curated_slurlist_hi_en.csv" diff --git a/backend/app/core/validators/README.md b/backend/app/core/validators/README.md index 39acc58..001c39f 100644 --- a/backend/app/core/validators/README.md +++ b/backend/app/core/validators/README.md @@ -319,7 +319,7 @@ Parameters / customization: - `metrics: dict` (required) — metric name-to-description mapping passed to the LLM judge - `max_score: int` (required) — maximum score per metric; used to define the scoring scale -- `llm_callable: str` (required) — model identifier passed to LiteLLM (e.g. `gpt-4o-mini`, `gpt-4o`) +- `llm_callable: str` (required) — model identifier passed to LiteLLM (e.g. `gpt-5.6-sol`) - `on_fail` Notes / limitations: @@ -359,7 +359,7 @@ Parameters / customization: - `topic_relevance_config_id: UUID` (required at runtime; resolves configuration and prompt version from tenant config) - `prompt_schema_version: int` (optional; defaults to `1`) -- `llm_callable: str` (default: `gpt-4o-mini`) — the model identifier passed to Guardrails' LLMCritic to perform the scope evaluation. This must be a model string supported by LiteLLM (e.g. `gpt-4o-mini`, `gpt-4o`). It controls which LLM is used to score whether the input is within the allowed topic scope; changing it affects cost, latency, and scoring quality. +- `llm_callable: str` (default: `gpt-5.6-sol`) — the model identifier passed to Guardrails' LLMCritic to perform the scope evaluation. This must be a model string supported by LiteLLM (e.g. `gpt-5.6-sol`). It controls which LLM is used to score whether the input is within the allowed topic scope; changing it affects cost, latency, and scoring quality. - `on_fail` Notes / limitations: @@ -387,10 +387,28 @@ Parameters / customization: - `configuration: str` — topic text. Inline, or resolved from `topic_relevance_config_id`. - `prompt_schema_version: int` (default `2`) — `1` = `configuration` lists allowed topics, `2` = forbidden topics, `3` = both (forbidden checked first). Resolved from the stored config when `topic_relevance_config_id` is used. - `threshold: int` (1–3, default `2`) — `3` also fails ambiguous messages. -- `llm_callable: str` (default `gpt-4o-mini`) +- `llm_callable: str` (default `gpt-5.6-sol`) - `fallback_message: str` (optional) — with `on_fail=rephrase`, returned as `safe_text` instead of the generic rephrase text; `rephrase_needed` stays `true`. The same LLM call translates it into the user's language (`prompts/topic_relevance_llm/fallback_translation.md`); if no translation comes back, the original text is used. Adds ~30–60 output tokens per call when set. - `on_fail` — use `rephrase`. `fix` returns an empty string, since this validator has no fix value. +LLM call: + +- `max_tokens=1024` (2048 when `fallback_message` is set). Reasoning models count hidden reasoning tokens against this limit, so it's sized well above observed use and only stops runaway calls. +- `reasoning_effort="medium"` is sent when the model supports reasoning; other models don't receive it. + +Evaluation (live run, October 2026): + +- 100 rows sampled from the education and healthcare topic relevance datasets (`backend/app/evaluation/datasets/topic_relevance/`): 50 per domain, 32 out of scope. Prompt schema v2, threshold 2, `max_tokens=1024`. + +| Model | Accuracy | Out-of-scope caught | In-scope wrongly blocked | F1 | Latency (typical / slowest 5%) | +|---|---|---|---|---|---| +| `gpt-5.6-sol` (`reasoning_effort="medium"`) | 0.89 | 25 / 32 | 4 / 68 | 0.82 | 2.7s / 4.1s | +| `gpt-4o-mini` | 0.89 | 21 / 32 | 0 / 68 | 0.79 | 1.3s / 1.6s | + +- `gpt-5.6-sol` catches more out-of-scope messages but blocks some in-scope ones. One example is "where can I get my baby's vaccination near my village?" on the healthcare config. +- No empty, cut-off or unparseable replies in 300 `gpt-5.6-sol` calls. The highest usage was 263 output tokens. +- `gpt-5.6-sol` is about 2x slower per call, and lowering `reasoning_effort` barely changes that. Expect roughly 2.5–4s per call. + ### 7) LlamaGuard 7B Validator (`llamaguard_7b`) Code: @@ -538,7 +556,7 @@ Recommendation: Parameters / customization: -- `llm_callable: str` (default: `gpt-4o-mini`) — model identifier passed to LiteLLM for the YES/NO evaluation +- `llm_callable: str` (default: `gpt-5.6-sol`) — model identifier passed to LiteLLM for the YES/NO evaluation - `prompt_template: str` (optional) — inline prompt with `{query}` and `{answer}` placeholders - `custom_prompt_id: UUID` (optional) — reference to a tenant-stored prompt config; resolved to `prompt_template` before execution - `on_fail` @@ -557,6 +575,8 @@ Notes / limitations: - **Requires `OPENAI_API_KEY` to be set in environment variables.** - `input` to the guardrail endpoint must be a JSON string: `{"query": "...", "answer": "..."}`. Both fields must be non-empty. - LLM-judge responses can vary; YES/NO parsing uses prefix matching. +- The LLM call uses `max_tokens=512` so reasoning models (e.g. `gpt-5.6-sol`) have room for hidden reasoning tokens, and sends `reasoning_effort="medium"` when the model supports reasoning. An empty reply fails as an unexpected response. +- Evaluation (live run, October 2026, 100 query/answer pairs): `gpt-5.6-sol` scored 0.91–0.92 accuracy against 0.84 for `gpt-4o-mini`. It's about 2x slower (1.2–1.4s typical, 2.7–2.8s for the slowest 5%). Neither model passed a wrong answer; every error was a correct answer rejected by the strict default prompt. Full results: `docs/validators/answer-relevance.md#evaluation-results`. - `on_fail=fix` has no programmatic fix for irrelevant answers — `safe_text` will be `""` and `metadata.reason` will identify this validator. - If `custom_prompt_id` is deleted after being referenced, the guardrail will return a 404 at resolution time. diff --git a/backend/app/core/validators/answer_relevance_custom_llm.py b/backend/app/core/validators/answer_relevance_custom_llm.py index 798daab..54db673 100644 --- a/backend/app/core/validators/answer_relevance_custom_llm.py +++ b/backend/app/core/validators/answer_relevance_custom_llm.py @@ -18,7 +18,10 @@ ANSWER_RELEVANCE_NOT_RELEVANT_ERROR, ANSWER_RELEVANCE_UNEXPECTED_RESPONSE_TEMPLATE, ) -from app.core.validators.llm_utils import traced_completion as completion +from app.core.validators.llm_utils import ( + supports_reasoning, + traced_completion as completion, +) DEFAULT_PROMPT_TEMPLATE = ( "Query: {query}\n" @@ -27,6 +30,11 @@ "Answer only YES or NO." ) +# Room for a one-word YES/NO plus the hidden reasoning tokens that reasoning +# models (e.g. gpt-5.6-sol) count against the same completion budget. +_MAX_TOKENS = 512 +_REASONING_EFFORT = "medium" + @register_validator(name="answer-relevance-custom-llm", data_type="string") class AnswerRelevanceCustomLLM(Validator): @@ -62,13 +70,17 @@ def _validate(self, value: str, metadata: dict | None = None) -> ValidationResul ) ) + completion_kwargs = { + "model": self.llm_callable, + "messages": [{"role": "user", "content": prompt}], + "max_tokens": _MAX_TOKENS, + } + if supports_reasoning(self.llm_callable): + completion_kwargs["reasoning_effort"] = _REASONING_EFFORT + try: - response = completion( - model=self.llm_callable, - messages=[{"role": "user", "content": prompt}], - max_tokens=10, - ) - response_text = response.choices[0].message.content.strip().upper() + response = completion(**completion_kwargs) + response_text = (response.choices[0].message.content or "").strip().upper() except Exception as e: return FailResult( error_message=ANSWER_RELEVANCE_LLM_CALL_FAILED_TEMPLATE.format(error=e) diff --git a/backend/app/core/validators/llm_utils.py b/backend/app/core/validators/llm_utils.py index d40d03c..19eb20a 100644 --- a/backend/app/core/validators/llm_utils.py +++ b/backend/app/core/validators/llm_utils.py @@ -1,4 +1,5 @@ from litellm import completion, get_supported_openai_params +from litellm import supports_reasoning as _litellm_supports_reasoning # Passed to litellm/OpenAI to force a strict JSON object response. JSON_OBJECT_RESPONSE_FORMAT = {"type": "json_object"} @@ -19,8 +20,7 @@ def traced_completion(**kwargs): # Models known to support JSON-object response_format that litellm may not list yet. _KNOWN_JSON_CAPABLE_MODELS = frozenset( { - "gpt-4o-mini", - "gpt-4o", + "gpt-5.6-sol", "gpt-5-mini", "gpt-5.4-mini", "gpt-5.4-nano", @@ -42,3 +42,15 @@ def supports_response_format(model: str) -> bool: return "response_format" in (get_supported_openai_params(model=model) or []) except Exception: return False + + +def supports_reasoning(model: str) -> bool: + """Return True if the given model is a reasoning model (accepts ``reasoning_effort``). + + Reasoning models count hidden reasoning tokens against the completion budget, + so callers use this to set a low effort and leave room for the visible answer. + """ + try: + return bool(_litellm_supports_reasoning(model=model)) + except Exception: + return False diff --git a/backend/app/core/validators/topic_relevance_llm.py b/backend/app/core/validators/topic_relevance_llm.py index 08c7fe6..e77e68e 100644 --- a/backend/app/core/validators/topic_relevance_llm.py +++ b/backend/app/core/validators/topic_relevance_llm.py @@ -13,12 +13,12 @@ Validator, register_validator, ) -from litellm import supports_reasoning as _litellm_supports_reasoning from app.core.config import settings from app.core.constants import EMPTY_MESSAGE_ERROR, TOPIC_OUT_OF_SCOPE_ERROR from app.core.validators.llm_utils import ( JSON_OBJECT_RESPONSE_FORMAT, + supports_reasoning, supports_response_format, traced_completion as completion, ) @@ -47,13 +47,10 @@ def _extract_first_json_object(text: str) -> dict: ) # Reasoning models (e.g. gpt-5.6-sol) accept reasoning_effort; non-reasoning models reject it. _REASONING_EFFORT = "medium" - - -def supports_reasoning(model: str) -> bool: - try: - return bool(_litellm_supports_reasoning(model=model)) - except Exception: - return False +# Ceiling on output tokens, hidden reasoning included. Sized well above observed +# gpt-5.6-sol usage at medium effort (max ~260) so it only stops runaway calls; +# doubled when localized_fallback is requested. +_MAX_TOKENS = 1024 @lru_cache(maxsize=8) @@ -106,6 +103,7 @@ def __init__( self._invalid_config_reason: Optional[str] = None self._system_prompt: Optional[str] = None self._supports_response_format: bool = False + self._max_tokens = _MAX_TOKENS * 2 if fallback_message else _MAX_TOKENS if not system_prompt or not system_prompt.strip(): self._invalid_config_reason = "system_prompt is blank or missing" @@ -143,6 +141,7 @@ def _validate( {"role": "system", "content": self._system_prompt}, {"role": "user", "content": value}, ], + "max_tokens": self._max_tokens, } if supports_reasoning(self.llm_callable): kwargs["reasoning_effort"] = _REASONING_EFFORT diff --git a/backend/app/evaluation/README.md b/backend/app/evaluation/README.md index d983ba5..dd3af18 100644 --- a/backend/app/evaluation/README.md +++ b/backend/app/evaluation/README.md @@ -234,7 +234,7 @@ python3 app/evaluation/topic_relevance/run.py --backend topic_relevance_llm `topic_relevance` (the `LLMCritic`-based validator) is only imported when that backend actually runs, so `--backend topic_relevance_llm` works without installing the `llm_critic` hub validator. -> **Note:** Requires `OPENAI_API_KEY` to be set. Uses `gpt-4o-mini` by default (`DEFAULT_CONFIG` in the script). +> **Note:** Requires `OPENAI_API_KEY` to be set. Uses `gpt-5.6-sol` by default (`settings.DEFAULT_LLM_CALLABLE`). --- diff --git a/backend/app/tests/test_llm_validators.py b/backend/app/tests/test_llm_validators.py index b211102..f16f323 100644 --- a/backend/app/tests/test_llm_validators.py +++ b/backend/app/tests/test_llm_validators.py @@ -20,7 +20,7 @@ _SAMPLE_TOPIC_CONFIG = dict( type="topic_relevance", configuration="Only answer about cooking.", - llm_callable="gpt-4o-mini", + llm_callable="gpt-5.6-sol", ) _TOPIC_RELEVANCE_SETTINGS_PATH = ( @@ -70,7 +70,7 @@ def test_topic_relevance_blank_config_returns_fail_result(): _SAMPLE_LLM_TOPIC_CONFIG = dict( type="topic_relevance_llm", configuration="Only answer about cooking.", - llm_callable="gpt-4o-mini", + llm_callable="gpt-5.6-sol", ) _TOPIC_RELEVANCE_LLM_SETTINGS_PATH = ( @@ -146,7 +146,7 @@ def test_topic_relevance_llm_custom_threshold_forwarded_to_validator(): "quality": {"description": "Is the response high quality?", "threshold": 2} }, max_score=3, - llm_callable="gpt-4o-mini", + llm_callable="gpt-5.6-sol", ) @@ -273,7 +273,7 @@ def test_answer_relevance_build_passes_inline_prompt_template(): def test_answer_relevance_build_passes_llm_callable(): config = AnswerRelevanceCustomLLMSafetyValidatorConfig( - **{**_SAMPLE_ANSWER_RELEVANCE_CONFIG, "llm_callable": "gpt-4o"} + **{**_SAMPLE_ANSWER_RELEVANCE_CONFIG, "llm_callable": "openai/gpt-5.6-sol"} ) with patch(_ANSWER_RELEVANCE_SETTINGS_PATH) as mock_settings, patch( @@ -284,4 +284,4 @@ def test_answer_relevance_build_passes_llm_callable(): config.build() _, kwargs = mock_validator.call_args - assert kwargs["llm_callable"] == "gpt-4o" + assert kwargs["llm_callable"] == "openai/gpt-5.6-sol" diff --git a/backend/app/tests/validators/test_answer_relevance_custom_llm.py b/backend/app/tests/validators/test_answer_relevance_custom_llm.py index dc0233d..92e5384 100644 --- a/backend/app/tests/validators/test_answer_relevance_custom_llm.py +++ b/backend/app/tests/validators/test_answer_relevance_custom_llm.py @@ -220,7 +220,7 @@ def test_fails_on_unexpected_llm_response(validator): def test_llm_callable_is_forwarded(): v = AnswerRelevanceCustomLLM( - llm_callable="gpt-4o", + llm_callable="openai/gpt-5.6-sol", input=QUERY, output=ANSWER_RELEVANT, ) @@ -231,4 +231,19 @@ def test_llm_callable_is_forwarded(): mock_llm.return_value = _make_llm_response("YES") v._validate(ANSWER_RELEVANT) - assert mock_llm.call_args.kwargs["model"] == "gpt-4o" + assert mock_llm.call_args.kwargs["model"] == "openai/gpt-5.6-sol" + + +# --------------------------------------------------------------------------- +# Empty LLM reply +# --------------------------------------------------------------------------- + + +def test_empty_content_returns_unexpected_response_fail(validator): + with patch( + "app.core.validators.answer_relevance_custom_llm.completion" + ) as mock_llm: + mock_llm.return_value = _make_llm_response(None) + result = validator._validate(ANSWER_RELEVANT) + + assert isinstance(result, FailResult) diff --git a/backend/app/tests/validators/test_topic_relevance_llm.py b/backend/app/tests/validators/test_topic_relevance_llm.py index e97a981..074a973 100644 --- a/backend/app/tests/validators/test_topic_relevance_llm.py +++ b/backend/app/tests/validators/test_topic_relevance_llm.py @@ -457,13 +457,21 @@ def test_fallback_instructions_added_to_system_prompt(): assert FALLBACK in validator._system_prompt -def test_no_max_tokens_cap_is_sent(): +def test_max_tokens_cap_is_sent(validator): + with patch("app.core.validators.topic_relevance_llm.completion") as mock_llm: + mock_llm.return_value = _make_llm_response('{"scope_violation": 3}') + validator._validate("How do I make pasta?") + + assert mock_llm.call_args.kwargs["max_tokens"] == 1024 + + +def test_fallback_doubles_max_tokens_cap(): validator = _fallback_validator() with patch("app.core.validators.topic_relevance_llm.completion") as mock_llm: mock_llm.return_value = _make_llm_response('{"scope_violation": 3}') validator._validate("How do I make pasta?") - assert "max_tokens" not in mock_llm.call_args.kwargs + assert mock_llm.call_args.kwargs["max_tokens"] == 2048 def test_reasoning_model_gets_medium_effort(): diff --git a/docs/DEVELOPER_GUIDE.md b/docs/DEVELOPER_GUIDE.md index bdb6cdf..8157f84 100644 --- a/docs/DEVELOPER_GUIDE.md +++ b/docs/DEVELOPER_GUIDE.md @@ -962,14 +962,14 @@ Validates whether a user message falls within the configured topic scope, using "type": "topic_relevance", "on_fail": "rephrase", "topic_relevance_config_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "llm_callable": "gpt-4o-mini" + "llm_callable": "gpt-5.6-sol" } ``` | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `topic_relevance_config_id` | `UUID` | required | ID of a stored topic relevance config | -| `llm_callable` | `string` | `"gpt-4o-mini"` | LLM model for evaluation | +| `llm_callable` | `string` | `"gpt-5.6-sol"` | LLM model for evaluation | | `prompt_schema_version` | `int` | `1` | Prompt template version | | `on_fail` | `string` | `"fix"` | Use `"rephrase"` or `"exception"` — no fix available | @@ -1068,7 +1068,7 @@ A general-purpose LLM-as-judge. Define custom metrics with plain-language descri "another_metric": "Description of another dimension" }, "max_score": 3, - "llm_callable": "gpt-4o-mini" + "llm_callable": "gpt-5.6-sol" } ``` @@ -1158,7 +1158,7 @@ Validates whether an LLM-generated answer is actually relevant to the user's que |----------|-------| | Type | `answer_relevance_custom_llm` | | Implementation | Local (LiteLLM-based) | -| Speed | Slow (500ms–2s, LLM API call) | +| Speed | Slow (about 1–3s with `gpt-5.6-sol`, LLM API call). See [evaluation results](validators/answer-relevance.md#evaluation-results). | | `fix_value` | No | | Requires API key | OpenAI API key | @@ -1168,14 +1168,14 @@ Validates whether an LLM-generated answer is actually relevant to the user's que { "type": "answer_relevance_custom_llm", "on_fail": "exception", - "llm_callable": "gpt-4o-mini", + "llm_callable": "gpt-5.6-sol", "custom_prompt_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6" } ``` | Parameter | Type | Default | Description | |-----------|------|---------|-------------| -| `llm_callable` | `string` | `"gpt-4o-mini"` | LLM model for evaluation | +| `llm_callable` | `string` | `"gpt-5.6-sol"` | LLM model for evaluation | | `prompt_template` | `string` | Default template | Inline template with `{query}` and `{answer}` placeholders | | `custom_prompt_id` | `UUID` | — | ID of a stored custom prompt | | `on_fail` | `string` | `"fix"` | Always use `"exception"` or `"rephrase"` — no fix available | @@ -1234,7 +1234,7 @@ GET /api/v1/guardrails/answer_relevance_prompts/?organization_id=1&project_id=10 1. Parses the JSON string to extract `query` and `answer`. Returns `FailResult` immediately if malformed or either field is empty. 2. Formats the `prompt_template` with the values. Returns `FailResult` if a placeholder is missing. -3. Sends to the LLM via LiteLLM with `max_tokens=10`. +3. Sends to the LLM via LiteLLM with `max_tokens=512` (room for hidden reasoning tokens), plus `reasoning_effort="medium"` when the model supports reasoning. 4. Strips and uppercases the response. Checks `.startswith("YES")` or `.startswith("NO")`. 5. Catches LLM API exceptions and returns them as `FailResult`. @@ -1370,7 +1370,7 @@ Copy-paste pipelines for common deployment scenarios. "groundedness": "No fabricated statistics or citations." }, "max_score": 3, - "llm_callable": "gpt-4o-mini" + "llm_callable": "gpt-5.6-sol" } ] } @@ -1560,7 +1560,7 @@ from guardrails.validator_base import FailResult, PassResult, ValidationResult, class PolicyCheckValidator(Validator): PROMPT = "Does the following text violate our policy of {policy}?\n\nText: {text}\n\nAnswer YES or NO only." - def __init__(self, policy: str, llm_callable: str = "gpt-4o-mini", **kwargs): + def __init__(self, policy: str, llm_callable: str = "gpt-5.6-sol", **kwargs): self.policy = policy self.client = OpenAI() self.model = llm_callable @@ -1584,7 +1584,7 @@ from app.core.validators.config.base_validator_config import BaseValidatorConfig class PolicyCheckConfig(BaseValidatorConfig): policy: str - llm_callable: str = "gpt-4o-mini" + llm_callable: str = "gpt-5.6-sol" def build(self): return PolicyCheckValidator( @@ -1628,7 +1628,7 @@ Then register in `validators.json` and add the type to the `ValidatorType` enum. ### LLM Validators - **Put LLM validators at the end of the chain** — Slowest and most expensive -- **Use `gpt-4o-mini` as default** — Fast and cheap; use `gpt-4o` only for critical nuanced evaluation +- **Use `gpt-5.6-sol` as default** — The standard model for LLM validators - **Test topic configs with diverse examples** — Too narrow frustrates users; too broad defeats the purpose ### Operations @@ -1789,7 +1789,7 @@ LLM critics catch what rules can't. A slur filter doesn't understand sarcasm. A ### "How do I reduce latency?" 1. Put cheap validators first (lexical < 10ms, ML 100–300ms, LLM 500ms–2s) -2. Use `gpt-4o-mini` over `gpt-4o` for LLM validators +2. Keep LLM validators on the default `gpt-5.6-sol` model 3. Use `"validation_method": "full"` for `nsfw_text` if per-sentence isn't needed 4. Set `"device": "cuda"` for `nsfw_text` on GPU infrastructure 5. Narrow `entity_types` for `pii_remover` — detecting 3 types is faster than 15 diff --git a/docs/architecture/ARCHITECTURE.md b/docs/architecture/ARCHITECTURE.md index c38a368..d5527f9 100644 --- a/docs/architecture/ARCHITECTURE.md +++ b/docs/architecture/ARCHITECTURE.md @@ -128,7 +128,7 @@ Each request carries a list of `ValidatorConfigItem` objects (a Pydantic discrim | **Lexical Slur** | `uli_slur_match` | Local (custom) | Detects slurs from a curated bilingual (Hindi/English) CSV. Normalises text (emoji removal, unicode NFKC, `ftfy` encoding fix) before matching. Supports severity filtering: `low`/`medium`/`high`/`all`. Redacts matches with `[REDACTED_SLUR]`. | | **PII Remover** | `pii_remover` | Local (custom) | Uses Microsoft Presidio (`presidio_analyzer` + `presidio_anonymizer`) with a HuggingFace NER recognizer (`neuronsbyisshu/indicbert-hi-ner-naamapadam`, IndicBERTv2 fine-tuned on Naamapadam) and spaCy `en_core_web_sm` for tokenization. Detects and redacts 15 entity types including India-specific identifiers (Aadhaar, PAN, Passport, Vehicle Registration, Voter ID). Replaces entities with labelled placeholders, e.g. `[REDACTED_PERSON_1]`. | | **Ban List** | `ban_list` | Hub (`guardrails/ban_list`) | Blocks exact words from a caller-supplied list or a stored `BanList` DB record (referenced by UUID). Words are fetched from the database in `_resolve_validator_configs()` before the guard runs. | -| **Topic Relevance** | `topic_relevance` | Local (custom) | Wraps the Hub's `LLMCritic` validator. Given a freeform topic-scope description (stored as a `TopicRelevance` DB record or supplied inline), constructs a scoring prompt and calls an LLM (default: `gpt-4o-mini`) via LiteLLM. Scores 1–3; score ≤ 1 is a fail. Supports versioned prompt templates (`v1.md`–`v3.md`). | +| **Topic Relevance** | `topic_relevance` | Local (custom) | Wraps the Hub's `LLMCritic` validator. Given a freeform topic-scope description (stored as a `TopicRelevance` DB record or supplied inline), constructs a scoring prompt and calls an LLM (default: `gpt-5.6-sol`) via LiteLLM. Scores 1–3; score ≤ 1 is a fail. Supports versioned prompt templates (`v1.md`–`v3.md`). | | **Profanity Free** | `profanity_free` | Hub (`guardrails/profanity_free`) | Detects general profanity in text. | ### Output guardrails (applied to the LLM's reply before it is sent to the user) diff --git a/docs/best-practices.md b/docs/best-practices.md index 84a830a..04e436b 100644 --- a/docs/best-practices.md +++ b/docs/best-practices.md @@ -78,9 +78,9 @@ Use `POST /api/v1/guardrails/ban_lists/` to create and manage ban lists. This le They're the slowest and most expensive. Only run them if cheaper validators haven't already blocked the content. -### Use `gpt-4o-mini` as your default LLM callable +### Use `gpt-5.6-sol` as your default LLM callable -It's fast and cheap for most moderation tasks. Use `gpt-4o` only for complex, nuanced evaluation where accuracy is critical. +It is the standard model for moderation tasks across all LLM validators. ### Test your topic relevance configs with diverse examples diff --git a/docs/custom-validators.md b/docs/custom-validators.md index a95e8d2..59a55cf 100644 --- a/docs/custom-validators.md +++ b/docs/custom-validators.md @@ -137,7 +137,7 @@ class PolicyCheckValidator(Validator): Answer with YES or NO only. """ - def __init__(self, policy: str, llm_callable: str = "gpt-4o-mini", **kwargs): + def __init__(self, policy: str, llm_callable: str = "gpt-5.6-sol", **kwargs): self.policy = policy self.client = OpenAI() self.model = llm_callable @@ -172,7 +172,7 @@ from guardrails import Validator class PolicyCheckConfig(BaseValidatorConfig): policy: str - llm_callable: str = "gpt-4o-mini" + llm_callable: str = "gpt-5.6-sol" def build(self) -> Validator: return PolicyCheckValidator( diff --git a/docs/faq.md b/docs/faq.md index cf6b9d9..3d4f840 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -56,7 +56,7 @@ Don't use them when: **2. Only validate what you need.** Don't run `llamaguard_7b` on every request if most of your traffic is safe. Consider running it only when faster validators pass a borderline score. -**3. Use `gpt-4o-mini` over `gpt-4o`** for LLM validators. It's 10x cheaper and about 2x faster for most moderation tasks. +**3. Use the default `gpt-5.6-sol`** for LLM validators rather than a larger custom model. **4. Use `"validation_method": "full"` for `nsfw_text`** if per-sentence granularity isn't needed. It's faster than `"sentence"` mode. diff --git a/docs/pipeline-patterns.md b/docs/pipeline-patterns.md index 442371a..b4b7692 100644 --- a/docs/pipeline-patterns.md +++ b/docs/pipeline-patterns.md @@ -105,7 +105,7 @@ Copy-paste pipelines for common deployment scenarios. "type": "topic_relevance", "on_fail": "rephrase", "topic_relevance_config_id": "your-domain-config-uuid", - "llm_callable": "gpt-4o-mini" + "llm_callable": "gpt-5.6-sol" } ] } @@ -140,7 +140,7 @@ Copy-paste pipelines for common deployment scenarios. "type": "topic_relevance", "on_fail": "rephrase", "topic_relevance_config_id": "healthcare-scope-uuid", - "llm_callable": "gpt-4o-mini" + "llm_callable": "gpt-5.6-sol" } ] } @@ -168,7 +168,7 @@ Copy-paste pipelines for common deployment scenarios. "groundedness": "The response should not make claims beyond what is generally known. No fabricated statistics or citations." }, "max_score": 3, - "llm_callable": "gpt-4o-mini" + "llm_callable": "gpt-5.6-sol" } ] } diff --git a/docs/use-cases.md b/docs/use-cases.md index 10e8465..396b646 100644 --- a/docs/use-cases.md +++ b/docs/use-cases.md @@ -27,7 +27,7 @@ Recommended validator pipelines for specific deployment scenarios. "type": "topic_relevance", "on_fail": "rephrase", "topic_relevance_config_id": "support-scope-uuid", - "llm_callable": "gpt-4o-mini" + "llm_callable": "gpt-5.6-sol" } ] } @@ -127,7 +127,7 @@ Recommended validator pipelines for specific deployment scenarios. "type": "topic_relevance", "on_fail": "rephrase", "topic_relevance_config_id": "edu-scope-uuid", - "llm_callable": "gpt-4o-mini" + "llm_callable": "gpt-5.6-sol" } ] } @@ -157,7 +157,7 @@ Recommended validator pipelines for specific deployment scenarios. "type": "topic_relevance", "on_fail": "rephrase", "topic_relevance_config_id": "health-scope-uuid", - "llm_callable": "gpt-4o-mini" + "llm_callable": "gpt-5.6-sol" } ] } @@ -184,7 +184,7 @@ Recommended validator pipelines for specific deployment scenarios. "medical_safety": "The response must not provide specific diagnoses, recommend specific medications, or substitute for professional medical advice. It should always recommend consulting a qualified doctor." }, "max_score": 3, - "llm_callable": "gpt-4o-mini" + "llm_callable": "gpt-5.6-sol" } ] } @@ -224,6 +224,6 @@ For code generation output, consider `llm_critic` with a security-focused metric "security": "The generated code must not include hardcoded credentials, SQL injection vulnerabilities, command injection, or insecure deserialization patterns." }, "max_score": 3, - "llm_callable": "gpt-4o-mini" + "llm_callable": "gpt-5.6-sol" } ``` diff --git a/docs/validators/answer-relevance.md b/docs/validators/answer-relevance.md index 400da71..b1bec02 100644 --- a/docs/validators/answer-relevance.md +++ b/docs/validators/answer-relevance.md @@ -10,7 +10,7 @@ Validates whether an LLM-generated answer is actually relevant to the user's que |----------|-------| | Type | `answer_relevance_custom_llm` | | Implementation | Local (LiteLLM-based) | -| Speed | Slow (500ms–2s, LLM API call) | +| Speed | Slow (about 1–3s with `gpt-5.6-sol`, LLM API call) | | `fix_value` | No | | Requires API key | OpenAI API key | @@ -22,14 +22,14 @@ Validates whether an LLM-generated answer is actually relevant to the user's que { "type": "answer_relevance_custom_llm", "on_fail": "exception", - "llm_callable": "gpt-4o-mini", + "llm_callable": "gpt-5.6-sol", "custom_prompt_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6" } ``` | Parameter | Type | Default | Description | |-----------|------|---------|-------------| -| `llm_callable` | `string` | `"gpt-4o-mini"` | LLM model for evaluation | +| `llm_callable` | `string` | `"gpt-5.6-sol"` | LLM model for evaluation | | `prompt_template` | `string` | Default template | Inline prompt template with `{query}` and `{answer}` placeholders | | `custom_prompt_id` | `UUID` | — | ID of a stored custom prompt. Resolved to `prompt_template` before validation. | | `on_fail` | `string` | `"fix"` | `"fix"`, `"exception"`, or `"rephrase"` | @@ -124,7 +124,9 @@ DELETE /api/v1/guardrails/answer_relevance_prompts/{id} 2. **Prompt construction** — The `prompt_template` is formatted with `{query}` and `{answer}` substituted. Returns a `FailResult` if a required placeholder is missing from the template. -3. **LLM call** — Sends the constructed prompt to the configured LLM via LiteLLM. Sets `max_tokens=10` since only a YES/NO response is needed. +3. **LLM call** — Sends the constructed prompt to the configured LLM via LiteLLM with `max_tokens=512`. Reasoning models such as `gpt-5.6-sol` count hidden reasoning tokens against this limit, so it leaves room for reasoning as well as the one-word answer. If the model supports reasoning, `reasoning_effort="medium"` is also sent; other models don't receive it. + + An empty reply is treated as an unexpected response (see step 4) rather than an LLM error. 4. **Response parsing** — The LLM response is stripped and uppercased. The validator looks for a `YES` or `NO` prefix. Unexpected responses are treated as failures. @@ -177,6 +179,26 @@ Result: FAIL — "Input must be a JSON string with 'query' and 'answer' fields." --- +## Evaluation Results + +Live run against the OpenAI API (October 2026), default prompt template, comparing the default `gpt-5.6-sol` with the previous default `gpt-4o-mini`. + +**Dataset:** 100 query/answer pairs on healthcare and education topics, in English and Hinglish. 50 pair each question with its correct answer (expected `YES`); 50 pair each question with a different question's answer, mostly from the same domain (expected `NO`). + +| Model | Accuracy | F1 | Wrong results | Latency (typical / slowest 5%) | Output tokens, highest | +|---|---|---|---|---|---| +| `gpt-5.6-sol` (`reasoning_effort="medium"`) | 0.91–0.92 | 0.92–0.93 | 8–9 | 1.2–1.4s / 2.7–2.8s | 296 (286 reasoning) | +| `gpt-4o-mini` | 0.84 | 0.86 | 16 | 0.7s / 0.9s | 1 | + +`gpt-5.6-sol` ranges are across two identical runs. + +- **No wrong answers passed.** Both models failed all 50 mismatched answers. Every wrong result was a correct answer being rejected. +- **The default prompt is strict.** Rejected correct answers included "Is TB treatment free in India?" answered with "Yes. TB diagnosis and treatment are free at government facilities…". The "fully satisfy the query and constraints" wording appears to make the judge reject answers it considers incomplete. Use a custom prompt if that's too strict for your domain. +- **No empty or cut-off replies** in 300 `gpt-5.6-sol` calls. The highest usage was 296 of the 512-token limit. +- **`gpt-5.6-sol` is about 2x slower per call.** Lowering `reasoning_effort` to `low` or `none` didn't change typical latency (about 1.1s even with no reasoning), so expect roughly 1–3s per call. + +--- + ## When to Use This Validator `answer_relevance_custom_llm` is specifically for **output validation** — checking that the LLM's response actually answered the question. diff --git a/docs/validators/llm-critic.md b/docs/validators/llm-critic.md index 1e7e4fa..c2f382e 100644 --- a/docs/validators/llm-critic.md +++ b/docs/validators/llm-critic.md @@ -27,7 +27,7 @@ A general-purpose LLM-as-judge validator. You define custom metrics with plain-l "another_metric": "Description of another dimension" }, "max_score": 3, - "llm_callable": "gpt-4o-mini" + "llm_callable": "gpt-5.6-sol" } ``` @@ -90,7 +90,7 @@ The metric description is the full instruction the LLM receives. Write it as a c "relevance": "Score how well the answer addresses the user's question. 0=not relevant, 1=partially relevant, 2=mostly relevant, 3=fully relevant and complete." }, "max_score": 3, - "llm_callable": "gpt-4o-mini", + "llm_callable": "gpt-5.6-sol", "on_fail": "exception" } diff --git a/docs/validators/topic-relevance.md b/docs/validators/topic-relevance.md index a7523ea..3ede8ad 100644 --- a/docs/validators/topic-relevance.md +++ b/docs/validators/topic-relevance.md @@ -23,14 +23,14 @@ Validates whether a user message falls within the configured topic scope, using "type": "topic_relevance", "on_fail": "rephrase", "topic_relevance_config_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "llm_callable": "gpt-4o-mini" + "llm_callable": "gpt-5.6-sol" } ``` | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `topic_relevance_config_id` | `UUID` | required | ID of a stored topic relevance config | -| `llm_callable` | `string` | `"gpt-4o-mini"` | LLM model for evaluation | +| `llm_callable` | `string` | `"gpt-5.6-sol"` | LLM model for evaluation | | `prompt_schema_version` | `int` | `1` | Prompt template version to use | | `on_fail` | `string` | `"fix"` | `"fix"`, `"exception"`, or `"rephrase"` |