From 33b1f1237b485449d38826169094de53e323b8b8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 1 May 2026 18:37:04 +0000 Subject: [PATCH] Fix OCR zero credit and Bedrock support checks --- litellm/cost_calculator.py | 2 +- .../anthropic_claude3_transformation.py | 2 +- .../anthropic_claude3_transformation.py | 2 +- ...ations_anthropic_claude3_transformation.py | 26 ++++++++++ .../test_anthropic_claude3_transformation.py | 50 +++++++++++++++++-- tests/test_litellm/llms/reducto/test_cost.py | 26 ++++++++++ 6 files changed, 102 insertions(+), 6 deletions(-) diff --git a/litellm/cost_calculator.py b/litellm/cost_calculator.py index d9710d0c1a4..074c0a11293 100644 --- a/litellm/cost_calculator.py +++ b/litellm/cost_calculator.py @@ -1812,7 +1812,7 @@ def ocr_cost( cost_per_credit = None if model_info is not None: cost_per_credit = model_info.get("ocr_cost_per_credit") - if credits is not None and cost_per_credit: + if credits is not None and cost_per_credit is not None: return cost_per_credit * credits, 0.0 pages_processed = response.usage_info.pages_processed diff --git a/litellm/llms/bedrock/chat/invoke_transformations/anthropic_claude3_transformation.py b/litellm/llms/bedrock/chat/invoke_transformations/anthropic_claude3_transformation.py index 9e8467fe394..02f0579c845 100644 --- a/litellm/llms/bedrock/chat/invoke_transformations/anthropic_claude3_transformation.py +++ b/litellm/llms/bedrock/chat/invoke_transformations/anthropic_claude3_transformation.py @@ -171,7 +171,7 @@ class AmazonAnthropicClaudeConfig(AmazonInvokeConfig, AnthropicConfig): anthropic_request.pop("stream", None) anthropic_request.pop("output_format", None) if not _supports_factory( - model=model, custom_llm_provider=None, key="supports_output_config" + model=model, custom_llm_provider="bedrock", key="supports_output_config" ): anthropic_request.pop("output_config", None) if "anthropic_version" not in anthropic_request: diff --git a/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py b/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py index e53b5881867..50f27979061 100644 --- a/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py +++ b/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py @@ -516,7 +516,7 @@ class AmazonAnthropicClaudeMessagesConfig( # but older models do not — strip it to avoid request rejection. # Ref: https://github.com/BerriAI/litellm/issues/22797 if not _supports_factory( - model=model, custom_llm_provider=None, key="supports_output_config" + model=model, custom_llm_provider="bedrock", key="supports_output_config" ): anthropic_messages_request.pop("output_config", None) diff --git a/tests/test_litellm/llms/bedrock/chat/invoke_transformations/test_bedrock_chat_invoke_transformations_anthropic_claude3_transformation.py b/tests/test_litellm/llms/bedrock/chat/invoke_transformations/test_bedrock_chat_invoke_transformations_anthropic_claude3_transformation.py index 80d6d26e7ea..22897a3cd62 100644 --- a/tests/test_litellm/llms/bedrock/chat/invoke_transformations/test_bedrock_chat_invoke_transformations_anthropic_claude3_transformation.py +++ b/tests/test_litellm/llms/bedrock/chat/invoke_transformations/test_bedrock_chat_invoke_transformations_anthropic_claude3_transformation.py @@ -2,6 +2,7 @@ import asyncio import json import os import sys +from unittest.mock import patch import pytest @@ -440,6 +441,31 @@ def test_output_config_removed_from_bedrock_chat_invoke_request(): assert result["max_tokens"] == 100 +def test_bedrock_chat_invoke_checks_output_config_support_with_bedrock_provider(): + config = AmazonAnthropicClaudeConfig() + messages = [{"role": "user", "content": "test"}] + optional_params = {"max_tokens": 100, "output_config": {"effort": "high"}} + + with patch( + "litellm.llms.bedrock.chat.invoke_transformations.anthropic_claude3_transformation._supports_factory", + return_value=True, + ) as mock_supports_factory: + result = config.transform_request( + model="us.anthropic.claude-opus-4-7", + messages=messages, + optional_params=optional_params, + litellm_params={}, + headers={}, + ) + + mock_supports_factory.assert_called_once_with( + model="us.anthropic.claude-opus-4-7", + custom_llm_provider="bedrock", + key="supports_output_config", + ) + assert result["output_config"] == {"effort": "high"} + + def test_output_format_removed_from_bedrock_invoke_request(): """ Test that output_format parameter is removed from Bedrock Invoke requests. diff --git a/tests/test_litellm/llms/bedrock/messages/invoke_transformations/test_anthropic_claude3_transformation.py b/tests/test_litellm/llms/bedrock/messages/invoke_transformations/test_anthropic_claude3_transformation.py index 78b882b40c8..2537ca9acda 100644 --- a/tests/test_litellm/llms/bedrock/messages/invoke_transformations/test_anthropic_claude3_transformation.py +++ b/tests/test_litellm/llms/bedrock/messages/invoke_transformations/test_anthropic_claude3_transformation.py @@ -668,6 +668,40 @@ def test_bedrock_messages_preserves_output_config_for_claude_4_6(): assert result.get("max_tokens") == 4096 +def test_bedrock_messages_checks_output_config_support_with_bedrock_provider(): + from unittest.mock import patch + + from litellm.types.router import GenericLiteLLMParams + + cfg = AmazonAnthropicClaudeMessagesConfig() + messages = [{"role": "user", "content": [{"type": "text", "text": "Hello"}]}] + optional_params = { + "max_tokens": 4096, + "output_config": { + "effort": "high", + }, + } + + with patch( + "litellm.llms.bedrock.messages.invoke_transformations.anthropic_claude3_transformation._supports_factory", + return_value=True, + ) as mock_supports_factory: + result = cfg.transform_anthropic_messages_request( + model="us.anthropic.claude-opus-4-7", + messages=messages, + anthropic_messages_optional_request_params=optional_params, + litellm_params=GenericLiteLLMParams(), + headers={}, + ) + + mock_supports_factory.assert_called_with( + model="us.anthropic.claude-opus-4-7", + custom_llm_provider="bedrock", + key="supports_output_config", + ) + assert result["output_config"] == {"effort": "high"} + + def test_bedrock_messages_strips_output_config_with_output_format(): """ When both output_config and output_format are present, output_format @@ -931,7 +965,10 @@ async def test_promote_message_start_cache_when_message_stop_omits_cache_fields( "delta": {"stop_reason": "end_turn", "stop_sequence": None}, "usage": {"input_tokens": 10, "output_tokens": 181}, } - yield {"type": "message_stop", "usage": {"input_tokens": 10, "output_tokens": 181}} + yield { + "type": "message_stop", + "usage": {"input_tokens": 10, "output_tokens": 181}, + } merged: list[dict] = [] async for chunk in cfg._promote_message_stop_usage(_stream()): @@ -985,7 +1022,11 @@ async def test_unified_bedrock_messages_cache_on_start_only_never_negative_cost( }, }, } - yield {"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": ""}} + yield { + "type": "content_block_start", + "index": 0, + "content_block": {"type": "text", "text": ""}, + } yield { "type": "content_block_delta", "index": 0, @@ -997,7 +1038,10 @@ async def test_unified_bedrock_messages_cache_on_start_only_never_negative_cost( "delta": {"stop_reason": "end_turn", "stop_sequence": None}, "usage": {"output_tokens": 181, "input_tokens": 10}, } - yield {"type": "message_stop", "usage": {"input_tokens": 10, "output_tokens": 181}} + yield { + "type": "message_stop", + "usage": {"input_tokens": 10, "output_tokens": 181}, + } logging_obj = LiteLLMLoggingObj( model="bedrock/anthropic.claude-3-5-sonnet-20240620-v1:0", diff --git a/tests/test_litellm/llms/reducto/test_cost.py b/tests/test_litellm/llms/reducto/test_cost.py index bbc65f90531..bb89100d020 100644 --- a/tests/test_litellm/llms/reducto/test_cost.py +++ b/tests/test_litellm/llms/reducto/test_cost.py @@ -27,6 +27,32 @@ def test_ocr_cost_prefers_credit_pricing_when_pages_processed_is_none(monkeypatc assert cost == 0.03 +def test_ocr_cost_prefers_zero_credit_pricing_over_page_pricing(monkeypatch): + monkeypatch.setattr( + litellm, + "get_model_info", + lambda model, custom_llm_provider=None: { + "ocr_cost_per_credit": 0.0, + "ocr_cost_per_page": 0.5, + }, + ) + + response = OCRResponse( + pages=[OCRPage(index=0, markdown="free credit priced")], + model="parse-v3", + usage_info=OCRUsageInfo(pages_processed=2, credits=10), + ) + + cost = completion_cost( + completion_response=response, + model="reducto/parse-v3", + custom_llm_provider="reducto", + call_type="ocr", + ) + + assert cost == 0.0 + + def test_ocr_cost_falls_back_to_page_pricing(monkeypatch): monkeypatch.setattr( litellm,