mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-27 12:47:13 +00:00
fix(provider): tolerate trimmed image parts in litellm message conversion (#2475)
SessionManager clears image_base64 on past turns to save memory, and
exclude_none serialization drops the hollowed field entirely, so a
replayed history part can arrive as {'type': 'image_base64'} with no
payload. The converter accessed the missing key unconditionally and
raised KeyError on every turn after an image was sent.
Prefer the base64 payload when present, fall back to an image_url that
survived on the same element, and drop hollow parts otherwise (same
strategy as the existing file-part handling). Fixes #2469.
This commit is contained in:
@@ -747,9 +747,24 @@ class LiteLLMRequester(requester.ProviderAPIRequester):
|
||||
converted_parts = []
|
||||
for part in content:
|
||||
if isinstance(part, dict) and part.get('type') == 'image_base64':
|
||||
part['image_url'] = {'url': part['image_base64']}
|
||||
part['type'] = 'image_url'
|
||||
del part['image_base64']
|
||||
# History trimming (SessionManager) clears image_base64
|
||||
# on past turns and exclude_none serialization drops
|
||||
# the key entirely, so the replayed part may carry no
|
||||
# payload. Prefer the base64 payload; fall back to an
|
||||
# image_url that survived on the same element; drop
|
||||
# hollow parts instead of raising KeyError (#2469).
|
||||
image_b64 = part.get('image_base64')
|
||||
fallback_url = None
|
||||
if not image_b64:
|
||||
raw_image_url = part.get('image_url')
|
||||
if isinstance(raw_image_url, dict):
|
||||
fallback_url = raw_image_url.get('url')
|
||||
if image_b64 or fallback_url:
|
||||
part['image_url'] = {'url': image_b64 or fallback_url}
|
||||
part['type'] = 'image_url'
|
||||
part.pop('image_base64', None)
|
||||
else:
|
||||
continue
|
||||
# OpenAI-compatible chat models reject non-image file parts
|
||||
# (audio/document base64 or url). These originate from Voice /
|
||||
# File attachments — including ones replayed from conversation
|
||||
|
||||
@@ -91,3 +91,42 @@ def test_convert_messages_plain_string_content_untouched():
|
||||
msg = provider_message.Message(role='user', content='just text')
|
||||
out = req._convert_messages([msg])
|
||||
assert out[0]['content'] == 'just text'
|
||||
|
||||
|
||||
def test_convert_messages_replayed_image_without_base64_does_not_crash():
|
||||
"""Replayed image parts hollowed out by history trimming must not raise KeyError (#2469).
|
||||
|
||||
SessionManager clears image_base64 on past turns, and URL-less platform
|
||||
images never had a URL, so the replayed part serializes as
|
||||
{'type': 'image_base64'} with no payload keys. The hollow part should be
|
||||
dropped while the sibling text part survives.
|
||||
"""
|
||||
req = _make_requester()
|
||||
image = provider_message.ContentElement.from_image_base64('data:image/jpeg;base64,AAAA')
|
||||
# Simulate SessionManager.trim_conversation_messages clearing binary payloads.
|
||||
image.image_base64 = None
|
||||
msg = provider_message.Message(
|
||||
role='user',
|
||||
content=[
|
||||
provider_message.ContentElement.from_text('describe the photo'),
|
||||
image,
|
||||
],
|
||||
)
|
||||
out = req._convert_messages([msg])
|
||||
assert [p.get('type') for p in out[0]['content']] == ['text']
|
||||
|
||||
|
||||
def test_convert_messages_replayed_image_with_url_falls_back_to_url():
|
||||
"""When base64 was trimmed but image_url survived, rebuild the OpenAI image_url part from the URL."""
|
||||
req = _make_requester()
|
||||
image = provider_message.ContentElement(
|
||||
type='image_base64',
|
||||
image_base64=None,
|
||||
image_url=provider_message.ImageURLContentObject(url='https://example.com/pic.jpg'),
|
||||
)
|
||||
msg = provider_message.Message(role='user', content=[image])
|
||||
out = req._convert_messages([msg])
|
||||
parts = out[0]['content']
|
||||
assert [p.get('type') for p in parts] == ['image_url']
|
||||
assert parts[0]['image_url'] == {'url': 'https://example.com/pic.jpg'}
|
||||
assert 'image_base64' not in parts[0]
|
||||
|
||||
Reference in New Issue
Block a user