From 13f004daed5d584823bdf5073b9a490f5d4ae4d9 Mon Sep 17 00:00:00 2001 From: philipph-askui Date: Wed, 10 Jun 2026 10:38:54 +0200 Subject: [PATCH 1/5] feat: automatically scale tool image outputs to stay within 2000px x 2000px limits --- src/askui/models/shared/tools.py | 6 +++++- src/askui/utils/image_utils.py | 27 +++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/askui/models/shared/tools.py b/src/askui/models/shared/tools.py index 74912911..18f6f2d7 100644 --- a/src/askui/models/shared/tools.py +++ b/src/askui/models/shared/tools.py @@ -33,7 +33,7 @@ ) from askui.tools import AgentOs from askui.tools.android.agent_os import AndroidAgentOs -from askui.utils.image_utils import ImageSource, base64_to_image +from askui.utils.image_utils import ImageSource, base64_to_image, downscale_image logger = logging.getLogger(__name__) @@ -100,6 +100,10 @@ def _convert_to_content( if isinstance(result, BaseModel): return [TextBlockParam(text=result.model_dump_json())] + # Downscale to stay within the 2000x2000 px per-image limit that applies + # when more than 20 images are sent in a single API request (Claude API). + # https://platform.claude.com/docs/en/build-with-claude/vision#before-you-upload + result = downscale_image(result, max_dimension=2000) return [ ImageBlockParam( source=Base64ImageSourceParam( diff --git a/src/askui/utils/image_utils.py b/src/askui/utils/image_utils.py index 19a5f92c..501f1fbb 100644 --- a/src/askui/utils/image_utils.py +++ b/src/askui/utils/image_utils.py @@ -227,6 +227,33 @@ def scale_image_to_fit( return _center_image_in_background(scaled_image, target_size) +def downscale_image( + image: Image.Image, + max_dimension: int = 2000, +) -> Image.Image: + """Downscale an image so its longest side does not exceed `max_dimension`. + + Preserves the original aspect ratio. Images that are already + within the limit are returned unchanged. + + Args: + image (Image.Image): The PIL Image to downscale. + max_dimension (int, optional): Maximum allowed size for the longest side. Defaults to `2000`. + + Returns: + Image.Image: The downscaled image, or the original if no scaling was needed. + """ + longest_side = max(image.width, image.height) + if longest_side <= max_dimension: + return image + scale_factor = max_dimension / longest_side + new_size = ( + int(image.width * scale_factor), + int(image.height * scale_factor), + ) + return image.resize(new_size, Image.Resampling.LANCZOS) + + def _scale_coordinates( coordinates: tuple[int, int], offset: tuple[int, int], From e5cf5841ca1911c6b5662798ca953e4d9ef2e177 Mon Sep 17 00:00:00 2001 From: philipph-askui Date: Wed, 10 Jun 2026 10:42:01 +0200 Subject: [PATCH 2/5] fix: remove automatic downscaling of tool images --- src/askui/models/shared/tools.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/askui/models/shared/tools.py b/src/askui/models/shared/tools.py index 18f6f2d7..91f896ae 100644 --- a/src/askui/models/shared/tools.py +++ b/src/askui/models/shared/tools.py @@ -33,7 +33,7 @@ ) from askui.tools import AgentOs from askui.tools.android.agent_os import AndroidAgentOs -from askui.utils.image_utils import ImageSource, base64_to_image, downscale_image +from askui.utils.image_utils import ImageSource, base64_to_image logger = logging.getLogger(__name__) @@ -103,7 +103,9 @@ def _convert_to_content( # Downscale to stay within the 2000x2000 px per-image limit that applies # when more than 20 images are sent in a single API request (Claude API). # https://platform.claude.com/docs/en/build-with-claude/vision#before-you-upload - result = downscale_image(result, max_dimension=2000) + # NOTE: downscaling is a responsibility of the tools themselves! + # Hence, we will not do it here anymore! + # result = downscale_image(result, max_dimension=2000) return [ ImageBlockParam( source=Base64ImageSourceParam( From 98741448e020427dd4ac436c0dce1ae65f8fc321 Mon Sep 17 00:00:00 2001 From: philipph-askui Date: Wed, 10 Jun 2026 10:46:50 +0200 Subject: [PATCH 3/5] feat: make downscaling a responsibility of the SDK again --- src/askui/models/shared/tools.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/askui/models/shared/tools.py b/src/askui/models/shared/tools.py index 91f896ae..18f6f2d7 100644 --- a/src/askui/models/shared/tools.py +++ b/src/askui/models/shared/tools.py @@ -33,7 +33,7 @@ ) from askui.tools import AgentOs from askui.tools.android.agent_os import AndroidAgentOs -from askui.utils.image_utils import ImageSource, base64_to_image +from askui.utils.image_utils import ImageSource, base64_to_image, downscale_image logger = logging.getLogger(__name__) @@ -103,9 +103,7 @@ def _convert_to_content( # Downscale to stay within the 2000x2000 px per-image limit that applies # when more than 20 images are sent in a single API request (Claude API). # https://platform.claude.com/docs/en/build-with-claude/vision#before-you-upload - # NOTE: downscaling is a responsibility of the tools themselves! - # Hence, we will not do it here anymore! - # result = downscale_image(result, max_dimension=2000) + result = downscale_image(result, max_dimension=2000) return [ ImageBlockParam( source=Base64ImageSourceParam( From 87d8dc5f4a06a99d50b149af0f372d27376fba0a Mon Sep 17 00:00:00 2001 From: philipph-askui Date: Wed, 10 Jun 2026 11:48:20 +0200 Subject: [PATCH 4/5] feat: remove automatic image scaling --- src/askui/models/shared/tools.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/askui/models/shared/tools.py b/src/askui/models/shared/tools.py index 18f6f2d7..74912911 100644 --- a/src/askui/models/shared/tools.py +++ b/src/askui/models/shared/tools.py @@ -33,7 +33,7 @@ ) from askui.tools import AgentOs from askui.tools.android.agent_os import AndroidAgentOs -from askui.utils.image_utils import ImageSource, base64_to_image, downscale_image +from askui.utils.image_utils import ImageSource, base64_to_image logger = logging.getLogger(__name__) @@ -100,10 +100,6 @@ def _convert_to_content( if isinstance(result, BaseModel): return [TextBlockParam(text=result.model_dump_json())] - # Downscale to stay within the 2000x2000 px per-image limit that applies - # when more than 20 images are sent in a single API request (Claude API). - # https://platform.claude.com/docs/en/build-with-claude/vision#before-you-upload - result = downscale_image(result, max_dimension=2000) return [ ImageBlockParam( source=Base64ImageSourceParam( From 06ee8f37b97f06b10ef7817558f90417c96bdda6 Mon Sep 17 00:00:00 2001 From: philipph-askui Date: Wed, 10 Jun 2026 11:53:44 +0200 Subject: [PATCH 5/5] chore: add information on image size limit to tool docs --- docs/07_tools.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/07_tools.md b/docs/07_tools.md index ffb8552c..32b04cc0 100644 --- a/docs/07_tools.md +++ b/docs/07_tools.md @@ -181,6 +181,18 @@ A tool’s __call__ method may return: - None - a list or tuple containing any of the above +**Image size limit:** When a tool returns a `PIL.Image.Image`, it is the tool’s responsibility to ensure the image does not exceed **2000×2000 px** (longest side ≤ 2000 px). The Claude API enforces a 2000×2000 px per-image limit when more than 20 images are sent in a single request, which is common in agentic loops. Use `downscale_image()` from `askui.utils.image_utils` to downscale images that may be too large: + +```python +from PIL import Image +from askui.utils.image_utils import downscale_image + +image: Image.Image = ... # your image +image = downscale_image(image, max_dimension=2000) +``` + +This preserves the original aspect ratio and only downscales images whose longest side exceeds the limit. + ### Complete Example Here’s a greeting tool that demonstrates all the key concepts: