{"record":{"id":"887d2827734f573c","repo":"BerriAI/litellm","slug":"ocr-response-pages-processed-is-none","errorCode":null,"errorMessage":"OCR response pages_processed is None","messagePattern":"OCR response pages_processed is None","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"litellm/cost_calculator.py","lineNumber":1855,"sourceCode":"    if model_info is not None:\n        ocr_cost_per_page = model_info.get(\"ocr_cost_per_page\")\n\n    pages_processed: Final = response.usage_info.pages_processed\n    if pages_processed is None:\n        if cost_per_credit is not None or ocr_cost_per_page is None:\n            # Surface missing usage data instead of silently under-reporting\n            # cost. The previous behavior raised ValueError; we now return 0.0\n            # for credit-priced or unpriced models, so log a warning to keep\n            # the regression visible to operators.\n            verbose_logger.warning(\n                \"OCR cost: model=%s custom_llm_provider=%s response.usage_info.\"\n                \"pages_processed is None and credits=%s; returning 0.0 cost.\",\n                model,\n                custom_llm_provider,\n                credits,\n            )\n            return 0.0, 0.0\n        raise ValueError(\"OCR response pages_processed is None\")\n\n    if ocr_cost_per_page is None:\n        # No per-page pricing configured. Either the model is on credit-based\n        # pricing (and credits weren't returned, so the credit branch above did\n        # not match) or the model has no OCR pricing entry at all. Surface a\n        # warning so that missing pricing entries are visible rather than\n        # silently producing zero cost for billable usage.\n        verbose_logger.warning(\n            \"OCR cost: model=%s custom_llm_provider=%s reported \"\n            \"pages_processed=%s but no ocr_cost_per_page is configured; \"\n            \"returning 0.0 cost.\",\n            model,\n            custom_llm_provider,\n            pages_processed,\n        )\n        return 0.0, 0.0\n\n    total_ocr_processing_cost: Final[float] = ocr_cost_per_page * pages_processed","sourceCodeStart":1837,"sourceCodeEnd":1873,"githubUrl":"https://github.com/BerriAI/litellm/blob/6c2dcb801bf2b75c18f1bb24140e7cf57465cc4d/litellm/cost_calculator.py#L1837-L1873","documentation":"For per-page OCR pricing, usage_info.pages_processed must be set. If credits-based pricing didn't apply (no credits or no ocr_cost_per_credit) and pages_processed is None, the calculator raises ValueError rather than silently returning zero cost — missing usage is surfaced as an error except in the credit-model case, which logs a warning and returns 0.0.","triggerScenarios":"OCRResponse with usage_info present but pages_processed=None and no usable credits; calling the OCR cost calculator on a response whose usage only carries credits while the model is priced per page (no ocr_cost_per_credit in model_info).","commonSituations":"Mixed pricing models (credit-based provider, per-page model entry); transformations that map credits but not page counts; new OCR models whose usage schema differs.","solutions":["Map pages_processed in your OCR transformation whenever the provider reports it.","Align the model's pricing entry: either supply usage pages for ocr_cost_per_page models or add ocr_cost_per_credit for credit-priced models.","If the provider returns only credits, register ocr_cost_per_credit via litellm.register_model so the credit branch handles it.","Treat missing usage as a provider bug — capture the raw response to confirm what it reports."],"exampleFix":"# before\nusage = OCRUsageInfo(credits=None)  # pages_processed omitted\n\n# after\nusage = OCRUsageInfo(pages_processed=provider_resp[\"pages\"], credits=provider_resp.get(\"credits\"))","handlingStrategy":"validation","validationCode":"info = litellm.get_model_info(model=model, custom_llm_provider=custom_llm_provider) or {}\nper_page = info.get(\"ocr_cost_per_page\")\nper_credit = info.get(\"ocr_cost_per_credit\")\ncredits = getattr(response.usage_info, \"credits\", None) if response.usage_info else None\npages = getattr(response.usage_info, \"pages_processed\", None) if response.usage_info else None\nif pages is None and not (credits is not None and per_credit is not None):\n    raise MissingOcrUsage(model)","typeGuard":"def ocr_priceable(resp, model: str) -> bool:\n    ui = getattr(resp, \"usage_info\", None)\n    if ui is None:\n        return False\n    if getattr(ui, \"pages_processed\", None) is not None:\n        return True\n    return getattr(ui, \"credits\", None) is not None","tryCatchPattern":"try:\n    cost = ocr_cost_fn(response=resp, model=model)\nexcept ValueError as e:\n    if \"pages_processed is None\" in str(e):\n        log_underreported_ocr(model)\n        cost = 0.0\n    else:\n        raise","preventionTips":["Ensure the OCR transformation always maps pages_processed when the provider reports it.","Match the usage fields you populate to the model's pricing keys (per-page vs per-credit).","Alert when OCR usage comes back empty so costs are never silently zero."],"tags":["litellm","cost-calculation","ocr","usage","pricing-data"],"backgroundTag":null,"analyzedSha":"6c2dcb801bf2b75c18f1bb24140e7cf57465cc4d","analyzedAt":"2026-08-15T07:12:03.035Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}