{"record":{"id":"1dafdb48140c6ad0","repo":"BerriAI/litellm","slug":"classifier-llm-config-classification-rubric-and-sy","errorCode":null,"errorMessage":"classifier_llm_config.classification_rubric and system_prompt are mutually exclusive: system_prompt replaces the built-in rubric the preset would select. Drop one.","messagePattern":"classifier_llm_config\\.classification_rubric and system_prompt are mutually exclusive: system_prompt replaces the built-in rubric the preset would select\\. Drop one\\.","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"litellm/router_strategy/complexity_router/config.py","lineNumber":338,"sourceCode":"    @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\nclass ComplexityRouterConfig(BaseModel):\n    \"\"\"Configuration for the ComplexityRouter.\"\"\"\n\n    # string = pin; list = random pick when adaptive=False, soft-floor home pool when adaptive=True\n    tiers: dict[str, str | list[str]] = Field(\n        default_factory=lambda: DEFAULT_TIER_MODELS.copy(),\n        description=(\n            \"Mapping of complexity tiers to a model or model pool. \"\n            \"A list is randomly picked from when adaptive=False, and used as a soft-floor home pool when adaptive=True\"\n        ),\n    )\n","sourceCodeStart":320,"sourceCodeEnd":356,"githubUrl":"https://github.com/BerriAI/litellm/blob/77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8/litellm/router_strategy/complexity_router/config.py#L320-L356","documentation":"Config validator on ComplexityRouterConfig: both a classification_rubric preset and a custom system_prompt are set. The system_prompt silently replaces the rubric the preset would choose, so the ambiguous configuration is rejected — keep one.","triggerScenarios":"Thrown at litellm/router_strategy/complexity_router/config.py:338 when the library encounters an invalid state.","commonSituations":"See trigger scenarios.","solutions":["Drop either classification_rubric or system_prompt from classifier_llm_config; they are mutually exclusive."],"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-14T00:17:10.932Z"}