2222_OTPM_LIMIT ["anthropic" ] = "max_tokens"
2323_CONCURRENT_ONLY_RATELIMIT_PROVIDERS = {"deepinfra" }
2424_FILE_UPLOAD_LIMIT_PROVIDERS = {"openai" : 20 } # MB
25+ _ANTHROPIC_MULTIMODAL_SUPPORTED_PREFIXES = (
26+ "claude-3" ,
27+ "claude-sonnet-4" ,
28+ "claude-haiku-4" ,
29+ "claude-opus-4" ,
30+ )
31+ _ANTHROPIC_PDF_BETA_HEADER = {"anthropic-beta" : "pdfs-2024-09-25" }
32+ _ANTHROPIC_IMAGE_TOKEN_ESTIMATE = 1024
33+ _ANTHROPIC_DOCUMENT_TOKEN_ESTIMATE = 2048
2534
2635
2736class LiteLLMOnlineRequestProcessor (BaseOnlineRequestProcessor ):
@@ -48,7 +57,7 @@ def __init__(self, config: OnlineRequestProcessorConfig):
4857 # NOTE: Removed global litellm.api_base assignment - now passed per-request
4958 # to allow multiple LLM instances with different base_urls
5059 self .client = instructor .from_litellm (litellm .acompletion )
51- self .header_based_max_requests_per_minute , self . header_based_max_tokens_per_minute = self . get_header_based_rate_limits ()
60+ self ._rate_limits_initialized = False
5261
5362 self ._validate_config (config )
5463 self ._set_manual_tpm (config )
@@ -66,6 +75,12 @@ def _validate_config(self, config):
6675 assert config .max_input_tokens_per_minute is None , "`max_input_tokens_per_minute` cannot be used with `combined` token strategy"
6776 assert config .max_output_tokens_per_minute is None , "`max_output_tokens_per_minute` cannot be used with `combined` token strategy"
6877
78+ def _ensure_rate_limits (self ):
79+ """Lazily initialize rate limits from API headers on first access."""
80+ if not self ._rate_limits_initialized :
81+ self .header_based_max_requests_per_minute , self .header_based_max_tokens_per_minute = self .get_header_based_rate_limits ()
82+ self ._rate_limits_initialized = True
83+
6984 @property
7085 def _provider (self ):
7186 return self .config .model .split ("/" )[0 ]
@@ -96,6 +111,7 @@ def max_requests_per_minute(self) -> int | None:
96111 or uses default value as last resort.
97112 """
98113 if not self ._concurrency_only_rate_limited :
114+ self ._ensure_rate_limits ()
99115 return super ().max_requests_per_minute
100116
101117 @property
@@ -106,6 +122,7 @@ def max_tokens_per_minute(self) -> int | None:
106122 or uses default value as last resort.
107123 """
108124 if not self ._concurrency_only_rate_limited :
125+ self ._ensure_rate_limits ()
109126 return super ().max_tokens_per_minute
110127
111128 @property
@@ -129,7 +146,54 @@ def file_upload_limit_check(self, base64_image: str) -> None:
129146
130147 @property
131148 def _multimodal_prompt_supported (self ) -> bool :
132- return litellm .supports_vision (self .config .model )
149+ if litellm .supports_vision (self .config .model ):
150+ return True
151+
152+ # LiteLLM's local model map can lag Anthropic releases and return a
153+ # false negative for newer or provider-prefixed Claude aliases.
154+ model_name = self .config .model .split ("/" , 1 )[- 1 ]
155+ if model_name .startswith ("claude-" ):
156+ return any (prefix in model_name for prefix in _ANTHROPIC_MULTIMODAL_SUPPORTED_PREFIXES )
157+
158+ return False
159+
160+ def _uses_anthropic_multimodal_format (self , model : str | None = None ) -> bool :
161+ model_name = (model or self .config .model ).split ("/" , 1 )[- 1 ]
162+ return model_name .startswith ("claude-" )
163+
164+ def _format_multimodal (self , data , mime_type = "image/png" ):
165+ mime_type = mime_type or "image/png"
166+ if not self ._uses_anthropic_multimodal_format ():
167+ return super ()._format_multimodal (data , mime_type = mime_type )
168+
169+ block_type = "image" if "image" in mime_type else "document"
170+ if data .url and not data .is_local :
171+ return {"type" : block_type , "source" : {"type" : "url" , "url" : data .url }}
172+
173+ base64_content = data .serialize ()
174+ self .file_upload_limit_check (base64_content )
175+ return {
176+ "type" : block_type ,
177+ "source" : {
178+ "type" : "base64" ,
179+ "media_type" : mime_type ,
180+ "data" : base64_content ,
181+ },
182+ }
183+
184+ @staticmethod
185+ def _has_anthropic_pdf_document (messages : list [dict ]) -> bool :
186+ for message in messages :
187+ content = message .get ("content" , [])
188+ if not isinstance (content , list ):
189+ continue
190+ for block in content :
191+ if block .get ("type" ) != "document" :
192+ continue
193+ source = block .get ("source" , {})
194+ if source .get ("media_type" ) == "application/pdf" or source .get ("type" ) == "url" :
195+ return True
196+ return False
133197
134198 def check_structured_output_support (self ):
135199 """Verify if the model supports structured output via litellm.
@@ -176,10 +240,42 @@ def estimate_total_tokens(self, messages: list) -> _TokenUsage:
176240 Returns:
177241 _TokenUsage: Total estimated tokens (input and output)
178242 """
179- input_tokens = litellm .token_counter (model = self .config .model , messages = messages )
243+ try :
244+ input_tokens = litellm .token_counter (model = self .config .model , messages = messages )
245+ except ValueError :
246+ if self ._uses_anthropic_multimodal_format () and self ._has_anthropic_pdf_document (messages ):
247+ input_tokens = self ._estimate_anthropic_document_tokens (messages )
248+ else :
249+ raise
180250 output_tokens = int (self .estimate_output_tokens ())
181251 return _TokenUsage (input = input_tokens , output = output_tokens )
182252
253+ def _estimate_anthropic_document_tokens (self , messages : list [dict ]) -> int :
254+ input_tokens = 0
255+ for message in messages :
256+ content = message .get ("content" , "" )
257+ role = message .get ("role" , "user" )
258+ if isinstance (content , str ):
259+ input_tokens += litellm .token_counter (model = self .config .model , messages = [{"role" : role , "content" : content }])
260+ continue
261+
262+ if not isinstance (content , list ):
263+ continue
264+
265+ for block in content :
266+ block_type = block .get ("type" )
267+ if block_type == "text" :
268+ input_tokens += litellm .token_counter (
269+ model = self .config .model ,
270+ messages = [{"role" : role , "content" : block .get ("text" , "" )}],
271+ )
272+ elif block_type == "image" :
273+ input_tokens += _ANTHROPIC_IMAGE_TOKEN_ESTIMATE
274+ elif block_type == "document" :
275+ input_tokens += _ANTHROPIC_DOCUMENT_TOKEN_ESTIMATE
276+
277+ return input_tokens
278+
183279 def test_call (self ):
184280 """Test call to get rate limits."""
185281 completion = litellm .completion (
@@ -282,6 +378,12 @@ def create_api_specific_request_online(self, generic_request: GenericRequest) ->
282378 },
283379 ]
284380
381+ if self ._uses_anthropic_multimodal_format (generic_request .model ) and self ._has_anthropic_pdf_document (generic_request .messages ):
382+ request ["extra_headers" ] = {
383+ ** request .get ("extra_headers" , {}),
384+ ** _ANTHROPIC_PDF_BETA_HEADER ,
385+ }
386+
285387 return request
286388
287389 async def call_single_request (
0 commit comments