Skip to content

Commit fa05930

Browse files
Fix simple_poem and use Sonnet 4.6 for multimodal testing (#714)
1 parent 5af79b8 commit fa05930

7 files changed

Lines changed: 330 additions & 826 deletions

File tree

‎examples/poem-generation/simple_poem.py‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@
88
# Use GPT-4o-mini for this example.
99
llm = curator.LLM(model_name="gpt-4o-mini")
1010
poem = llm(["Write a poem about the importance of data in AI."])
11-
print(poem.to_pandas().iloc[0]["response"])
11+
print(poem.dataset.to_pandas().iloc[0]["response"])
1212

13-
# Use Claude 3.5 Sonnet for this example.
14-
llm = curator.LLM(model_name="claude-3-5-sonnet-20240620", backend="litellm")
13+
# Use Claude Sonnet 4.6 for this example.
14+
llm = curator.LLM(model_name="claude-sonnet-4-6", backend="anthropic")
1515
poem = llm(["Write a poem about the importance of data in AI."])
16-
print(poem.to_pandas().iloc[0]["response"])
16+
print(poem.dataset.to_pandas().iloc[0]["response"])
1717

1818
# Note that we can also pass a list of prompts to generate multiple responses.
1919
poems = llm(

‎src/bespokelabs/curator/request_processor/online/base_online_request_processor.py‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -273,10 +273,13 @@ def requests_to_responses(
273273
Args:
274274
generic_request_files: List of request files to process
275275
"""
276+
max_requests_per_minute = self.max_requests_per_minute
277+
max_tokens_per_minute = self.max_tokens_per_minute
278+
276279
self.tracker = OnlineStatusTracker(
277280
token_limit_strategy=self.token_limit_strategy,
278-
max_requests_per_minute=self.max_requests_per_minute,
279-
max_tokens_per_minute=self.max_tokens_per_minute,
281+
max_requests_per_minute=max_requests_per_minute,
282+
max_tokens_per_minute=max_tokens_per_minute,
280283
compatible_provider=self.compatible_provider,
281284
viewer_client=self._viewer_client,
282285
model=self.prompt_formatter.model_name,

‎src/bespokelabs/curator/request_processor/online/litellm_online_request_processor.py‎

Lines changed: 105 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,15 @@
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

2736
class 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(

‎tests/integrations/helper.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ def create_camel(temp_working_dir, batch=False, batch_check_interval=1):
170170

171171
_DEFAULT_MODEL_MAP = {
172172
"openai": "gpt-3.5-turbo",
173-
"anthropic": "claude-3-5-sonnet-20241022",
173+
"anthropic": "claude-sonnet-4-6",
174174
"litellm": "gpt-3.5-turbo",
175175
"vllm": "Qwen/Qwen2.5-1.5B-Instruct",
176176
"gemini": "gemini-1.5-flash-002",

0 commit comments

Comments
 (0)