{"record":{"id":"f10a21ec709e1b7b","repo":"BerriAI/litellm","slug":"classifier-llm-config-system-prompt-must-be-non-em","errorCode":null,"errorMessage":"classifier_llm_config.system_prompt must be non-empty; omit it to use the default rubric","messagePattern":"classifier_llm_config\\.system_prompt must be non-empty; omit it to use the default rubric","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"litellm/router_strategy/complexity_router/config.py","lineNumber":326,"sourceCode":"            \"neither the default rubric nor the context-window closing line is appended, so the prompt \"\n            \"owns the whole taxonomy and the tier names SIMPLE/MEDIUM/COMPLEX/REASONING become whatever \"\n            \"buckets it defines: a prompt that classifies data sensitivity routes on that instead of on \"\n            \"difficulty. Two consequences of full replacement. The default rubric's closing paragraph is \"\n            \"the classifier's prompt-injection defense, telling it that the caller's quoted system prompt \"\n            \"and prior turns are material to judge and never instructions; a replacement that omits it \"\n            \"lets a caller ask for a tier and get it. And the heuristic fallback still scores complexity, \"\n            \"so a router on some other taxonomy wants classifier_fallback='default_model'. Leave unset \"\n            \"for the built-in rubric. Only applies when classifier_type is 'llm'.\"\n        ),\n    )\n\n    @field_validator(\"system_prompt\")\n    @classmethod\n    def _reject_blank_system_prompt(cls, value: str | None) -> str | None:\n        # A blank string is a misconfiguration, not a request for the default: it would send an\n        # empty system role and leave the classifier with no rubric at all. None means default.\n        if value is not None and not value.strip():\n            raise ValueError(\"classifier_llm_config.system_prompt must be non-empty; omit it to use the default rubric\")\n        return value\n\n    @model_validator(mode=\"after\")\n    def _reject_rubric_with_system_prompt(self) -> \"ClassifierLLMConfig\":\n        # A custom prompt is the classifier's whole system role, so a preset set alongside it would never\n        # reach the wire. Rejecting it beats honoring one of two settings the operator asked for.\n        #\n        # None, not model_fields_set, is what marks the preset unchosen: this model is dumped and\n        # re-validated in place (see /auto_router/test_routing), and a dump re-states every field, so\n        # keying on fields_set would reject on the second pass what it accepted on the first.\n        if self.system_prompt is not None and self.classification_rubric is not None:\n            raise ValueError(\n                \"classifier_llm_config.classification_rubric and system_prompt are mutually exclusive: system_prompt replaces \"\n                \"the built-in rubric the preset would select. Drop one.\"\n            )\n        return self\n\n","sourceCodeStart":308,"sourceCodeEnd":344,"githubUrl":"https://github.com/BerriAI/litellm/blob/77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8/litellm/router_strategy/complexity_router/config.py#L308-L344","documentation":"Field validator on the classifier LLM config: system_prompt was set to a whitespace-only string. A blank prompt is a misconfiguration, not a request for the default rubric — pass None to use the built-in rubric.","triggerScenarios":"Thrown at litellm/router_strategy/complexity_router/config.py:326 when the library encounters an invalid state.","commonSituations":"See trigger scenarios.","solutions":["Provide a non-empty system_prompt or omit it to use the default rubric."],"exampleFix":null,"handlingStrategy":"validation","validationCode":null,"typeGuard":null,"tryCatchPattern":null,"preventionTips":[],"tags":[],"backgroundTag":null,"analyzedSha":"77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8","analyzedAt":"2026-08-18T11:44:31.656Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}