{"record":{"id":"e2ef1cd5f3edd93d","repo":"BerriAI/litellm","slug":"ptu-effective-to-must-be-after-ptu-effective-from","errorCode":null,"errorMessage":"ptu_effective_to must be after ptu_effective_from","messagePattern":"ptu_effective_to must be after ptu_effective_from","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"litellm/proxy/management_endpoints/model_management_endpoints.py","lineNumber":322,"sourceCode":"def _validate_ptu_model_info(model_info: Mapping[str, object]) -> None:\n    \"\"\"Enforce the PTU cross-field invariant on the effective model_info.\n\n    ptu_count and cost_per_ptu_per_hour must be set together, and a team_id and a\n    ptu_effective_from are required when they are. The start is mandatory rather than\n    defaulted because flat cost accrues from it: inferring one would let a deployment\n    configured today be billed for days it did not exist. Per-field bounds (positive\n    count, non-negative rate) are enforced by ModelInfo itself.\n\n    Window ordering is checked before the count/rate gate. A patch that touches only one\n    end of the window carries no count or rate, and ModelInfo sees one field at a time, so\n    leaving it to either would let an inverted window reach the row; the next load then\n    fails to parse it and drops the deployment out of the router, where no further patch\n    can repair it because each one re-parses the stored value first.\n    \"\"\"\n    effective_from: Final = _coerce_ptu_datetime(model_info.get(\"ptu_effective_from\"))\n    effective_to: Final = _coerce_ptu_datetime(model_info.get(\"ptu_effective_to\"))\n    if effective_from is not None and effective_to is not None and effective_to <= effective_from:\n        raise HTTPException(status_code=400, detail=\"ptu_effective_to must be after ptu_effective_from\")\n\n    has_count: Final = model_info.get(\"ptu_count\") is not None\n    has_rate: Final = model_info.get(\"cost_per_ptu_per_hour\") is not None\n    if not has_count and not has_rate:\n        return\n    if has_count != has_rate:\n        raise HTTPException(status_code=400, detail=\"ptu_count and cost_per_ptu_per_hour must be set together\")\n    if effective_from is None:\n        raise HTTPException(\n            status_code=400,\n            detail=(\n                \"ptu_effective_from is required when PTU fields are set. Flat cost accrues from that \"\n                \"instant, so without it the start would have to be inferred and a deployment configured \"\n                \"today could be billed for days it did not exist\"\n            ),\n        )\n    if not model_info.get(\"team_id\"):\n        raise HTTPException(","sourceCodeStart":304,"sourceCodeEnd":340,"githubUrl":"https://github.com/BerriAI/litellm/blob/77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8/litellm/proxy/management_endpoints/model_management_endpoints.py#L304-L340","documentation":"Cross-field invariant guard on PTU model_info: the effective window has ptu_effective_to not strictly after ptu_effective_from. Checked before the count/rate gate because a patch touching only one window end carries no count/rate and would otherwise let an inverted window persist, where the next router load drops the deployment.","triggerScenarios":"Thrown at litellm/proxy/management_endpoints/model_management_endpoints.py:322 when the library encounters an invalid state.","commonSituations":"See trigger scenarios.","solutions":["Set ptu_effective_to to a timestamp later than ptu_effective_from."],"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"}