diff --git a/litellm/litellm_core_utils/token_counter.py b/litellm/litellm_core_utils/token_counter.py index 17f3dea72ec..332f2803f3c 100644 --- a/litellm/litellm_core_utils/token_counter.py +++ b/litellm/litellm_core_utils/token_counter.py @@ -213,12 +213,19 @@ def get_image_dimensions( img_data = body except Exception: pass - if img_data is None: - # Not a URL or fetch failed — assume base64 - _header, encoded = data.split(",", 1) - img_data = base64.b64decode(encoded) + if img_data is None and not data.startswith(("http://", "https://")): + # Not a URL — assume base64. Accept both data-URL form + # ('data:;base64,') and bare base64; on a decode + # error, leave img_data unset so the default dimensions are used. + encoded = data.partition(",")[2] if data.startswith("data:") else data + try: + img_data = base64.b64decode(encoded) + except ValueError: + verbose_logger.debug("Failed to decode base64 image data; using default dimensions") - img_type: Final = get_image_type(img_data) + # A URL that could not be fetched (or base64 that could not be decoded) + # leaves img_data unset — fall through to the default dimensions below. + img_type: Final = get_image_type(img_data) if img_data is not None else None if img_type == "png": w, h = struct.unpack(">LL", img_data[16:24]) diff --git a/tests/test_litellm/litellm_core_utils/test_token_counter.py b/tests/test_litellm/litellm_core_utils/test_token_counter.py index 71e686563a5..c585f7a7792 100644 --- a/tests/test_litellm/litellm_core_utils/test_token_counter.py +++ b/tests/test_litellm/litellm_core_utils/test_token_counter.py @@ -526,6 +526,53 @@ def test_img_url_token_counter(img_url, monkeypatch): assert height is not None +def test_get_image_dimensions_bare_base64(): + """ + Bare base64 (no 'data:' URL prefix) is documented input for + get_image_dimensions and must not crash on the ','-split of a data URL. + """ + import base64 + import struct + + from litellm.litellm_core_utils.token_counter import get_image_dimensions + + # Minimal PNG header carrying 100x200 dimensions. + png = base64.b64encode( + b"\x89PNG\r\n\x1a\n" + b"\x00" * 8 + struct.pack(">LL", 100, 200) + b"\x00" * 16 + ) + assert get_image_dimensions(data=png.decode()) == (100, 200) + + +def test_get_image_dimensions_unfetchable_url_returns_defaults(monkeypatch): + """A URL that cannot be fetched falls back to the default dimensions instead of raising.""" + + def _raise(client, url, **kwargs): + raise RuntimeError("connection failed") + + monkeypatch.setattr( + "litellm.litellm_core_utils.token_counter.safe_get", + _raise, + ) + from litellm.constants import DEFAULT_IMAGE_HEIGHT, DEFAULT_IMAGE_WIDTH + from litellm.litellm_core_utils.token_counter import get_image_dimensions + + assert get_image_dimensions(data="https://invalid.invalid/x.png") == ( + DEFAULT_IMAGE_WIDTH, + DEFAULT_IMAGE_HEIGHT, + ) + + +def test_get_image_dimensions_undecodable_data_returns_defaults(): + """Data that is neither a fetchable URL nor decodable base64 uses the defaults.""" + from litellm.constants import DEFAULT_IMAGE_HEIGHT, DEFAULT_IMAGE_WIDTH + from litellm.litellm_core_utils.token_counter import get_image_dimensions + + assert get_image_dimensions(data="not-an-image!") == ( + DEFAULT_IMAGE_WIDTH, + DEFAULT_IMAGE_HEIGHT, + ) + + def test_token_encode_disallowed_special(): encode(model="gpt-3.5-turbo", text="Hello, world! <|endoftext|>") token_counter(model="gpt-3.5-turbo", text="Hello, world! <|endoftext|>")