From 48a3c0f3edda3e7c529b42f754f77b9d475d2e1a Mon Sep 17 00:00:00 2001 From: James Chainey Date: Fri, 31 Oct 2025 11:15:09 -0700 Subject: [PATCH 1/5] added file_mount validation limits --- src/runloop_api_client/_constants.py | 6 +++ .../resources/blueprints.py | 32 +++++++++++ tests/api_resources/test_blueprints.py | 54 +++++++++++++++++++ uv.lock | 2 +- 4 files changed, 93 insertions(+), 1 deletion(-) diff --git a/src/runloop_api_client/_constants.py b/src/runloop_api_client/_constants.py index 4fe93f3ea..e03a0d7a6 100644 --- a/src/runloop_api_client/_constants.py +++ b/src/runloop_api_client/_constants.py @@ -12,3 +12,9 @@ INITIAL_RETRY_DELAY = 1.0 MAX_RETRY_DELAY = 60.0 + +# Maximum allowed size (in bytes) for individual entries in `file_mounts` when creating Blueprints +FILE_MOUNT_MAX_SIZE_BYTES = 512 * 1024 + +# Maximum allowed total size (in bytes) across all `file_mounts` when creating Blueprints +FILE_MOUNT_TOTAL_MAX_SIZE_BYTES = 1024 * 1024 diff --git a/src/runloop_api_client/resources/blueprints.py b/src/runloop_api_client/resources/blueprints.py index 34f98b498..25e04b92a 100644 --- a/src/runloop_api_client/resources/blueprints.py +++ b/src/runloop_api_client/resources/blueprints.py @@ -23,6 +23,7 @@ async_to_raw_response_wrapper, async_to_streamed_response_wrapper, ) +from .._constants import FILE_MOUNT_MAX_SIZE_BYTES, FILE_MOUNT_TOTAL_MAX_SIZE_BYTES from ..pagination import SyncBlueprintsCursorIDPage, AsyncBlueprintsCursorIDPage from .._exceptions import RunloopError from ..lib.polling import PollingConfig, poll_until @@ -50,6 +51,33 @@ class BlueprintRequestArgs(TypedDict, total=False): __all__ = ["BlueprintsResource", "AsyncBlueprintsResource", "BlueprintRequestArgs"] +def _validate_file_mounts(file_mounts: Optional[Dict[str, str]] | Omit) -> None: + """Validate file_mounts are within size constraints. + + Currently enforces a maximum per-file size to avoid server-side issues with + large inline file contents. Also enforces a maximum total size across all + file_mounts. + """ + + if file_mounts is omit or file_mounts is None: + return + + total_size_bytes = 0 + for mount_path, content in file_mounts.items(): + # Measure size in bytes using UTF-8 encoding since payloads are JSON strings + size_bytes = len(content.encode("utf-8")) + if size_bytes > FILE_MOUNT_MAX_SIZE_BYTES: + raise ValueError( + f"file_mount '{mount_path}' exceeds maximum size of {FILE_MOUNT_MAX_SIZE_BYTES} bytes. Use object_mounts instead." + ) + total_size_bytes += size_bytes + + if total_size_bytes > FILE_MOUNT_TOTAL_MAX_SIZE_BYTES: + raise ValueError( + f"total file_mounts size exceeds maximum of {FILE_MOUNT_TOTAL_MAX_SIZE_BYTES} bytes. Use object_mounts instead." + ) + + class BlueprintsResource(SyncAPIResource): @cached_property def with_raw_response(self) -> BlueprintsResourceWithRawResponse: @@ -144,6 +172,8 @@ def create( idempotency_key: Specify a custom idempotency key for this request """ + _validate_file_mounts(file_mounts) + return self._post( "/v1/blueprints", body=maybe_transform( @@ -758,6 +788,8 @@ async def create( idempotency_key: Specify a custom idempotency key for this request """ + _validate_file_mounts(file_mounts) + return await self._post( "/v1/blueprints", body=await async_maybe_transform( diff --git a/tests/api_resources/test_blueprints.py b/tests/api_resources/test_blueprints.py index 30ae21d3e..aec7b4433 100644 --- a/tests/api_resources/test_blueprints.py +++ b/tests/api_resources/test_blueprints.py @@ -108,6 +108,33 @@ def test_streaming_response_create(self, client: Runloop) -> None: assert cast(Any, response.is_closed) is True + @parametrize + def test_create_rejects_large_file_mount(self, client: Runloop) -> None: + # 512KB + 1 byte + too_large_content = "a" * (512 * 1024 + 1) + with pytest.raises(ValueError, match=r"exceeds maximum size"): + client.blueprints.create( + name="name", + file_mounts={"/tmp/large.txt": too_large_content}, + ) + + @parametrize + def test_create_rejects_total_file_mount_size(self, client: Runloop) -> None: + # Two files at exactly per-file max, plus 1 extra byte across a third file to exceed 1MB total + per_file_max = 512 * 1024 + content_a = "a" * per_file_max + content_b = "b" * per_file_max + content_c = "c" * 1 + with pytest.raises(ValueError, match=r"total file_mounts size exceeds maximum"): + client.blueprints.create( + name="name", + file_mounts={ + "/tmp/a.txt": content_a, + "/tmp/b.txt": content_b, + "/tmp/c.txt": content_c, + }, + ) + @parametrize def test_method_retrieve(self, client: Runloop) -> None: blueprint = client.blueprints.retrieve( @@ -536,6 +563,33 @@ async def test_streaming_response_create(self, async_client: AsyncRunloop) -> No assert cast(Any, response.is_closed) is True + @parametrize + async def test_create_rejects_large_file_mount(self, async_client: AsyncRunloop) -> None: + # 512KB + 1 byte + too_large_content = "a" * (512 * 1024 + 1) + with pytest.raises(ValueError, match=r"exceeds maximum size"): + await async_client.blueprints.create( + name="name", + file_mounts={"/tmp/large.txt": too_large_content}, + ) + + @parametrize + async def test_create_rejects_total_file_mount_size(self, async_client: AsyncRunloop) -> None: + # Two files at exactly per-file max, plus 1 extra byte across a third file to exceed 1MB total + per_file_max = 512 * 1024 + content_a = "a" * per_file_max + content_b = "b" * per_file_max + content_c = "c" * 1 + with pytest.raises(ValueError, match=r"total file_mounts size exceeds maximum"): + await async_client.blueprints.create( + name="name", + file_mounts={ + "/tmp/a.txt": content_a, + "/tmp/b.txt": content_b, + "/tmp/c.txt": content_c, + }, + ) + @parametrize async def test_method_retrieve(self, async_client: AsyncRunloop) -> None: blueprint = await async_client.blueprints.retrieve( diff --git a/uv.lock b/uv.lock index 2e395a445..4425156fb 100644 --- a/uv.lock +++ b/uv.lock @@ -1006,7 +1006,7 @@ wheels = [ [[package]] name = "runloop-api-client" -version = "0.65.0" +version = "0.66.1" source = { editable = "." } dependencies = [ { name = "anyio" }, From 61c00caa425fe4eafadd445298328fdb171433f5 Mon Sep 17 00:00:00 2001 From: James Chainey Date: Fri, 31 Oct 2025 13:41:41 -0700 Subject: [PATCH 2/5] improved error message handling to use notification pattern instead of fast fail with exception --- src/runloop_api_client/_utils/__init__.py | 2 + .../resources/blueprints.py | 49 +++++++++++++++---- tests/api_resources/test_blueprints.py | 8 +-- 3 files changed, 45 insertions(+), 14 deletions(-) diff --git a/src/runloop_api_client/_utils/__init__.py b/src/runloop_api_client/_utils/__init__.py index dc64e29a1..5541c2cc9 100644 --- a/src/runloop_api_client/_utils/__init__.py +++ b/src/runloop_api_client/_utils/__init__.py @@ -1,3 +1,4 @@ +# isort: skip_file from ._sync import asyncify as asyncify from ._proxy import LazyProxy as LazyProxy from ._utils import ( @@ -57,6 +58,7 @@ maybe_transform as maybe_transform, async_maybe_transform as async_maybe_transform, ) +from ._validation import ValidationNotification as ValidationNotification from ._reflection import ( function_has_argument as function_has_argument, assert_signatures_in_sync as assert_signatures_in_sync, diff --git a/src/runloop_api_client/resources/blueprints.py b/src/runloop_api_client/resources/blueprints.py index 25e04b92a..86c56b51a 100644 --- a/src/runloop_api_client/resources/blueprints.py +++ b/src/runloop_api_client/resources/blueprints.py @@ -1,4 +1,5 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. +# isort: skip_file from __future__ import annotations @@ -14,7 +15,7 @@ blueprint_create_from_inspection_params, ) from .._types import NOT_GIVEN, Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given -from .._utils import maybe_transform, async_maybe_transform +from .._utils import maybe_transform, async_maybe_transform, ValidationNotification from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -51,32 +52,56 @@ class BlueprintRequestArgs(TypedDict, total=False): __all__ = ["BlueprintsResource", "AsyncBlueprintsResource", "BlueprintRequestArgs"] -def _validate_file_mounts(file_mounts: Optional[Dict[str, str]] | Omit) -> None: - """Validate file_mounts are within size constraints. +def _format_bytes(num_bytes: int) -> str: + """Format a byte count in a human-friendly way (KB/MB/GB). + + Uses binary units (1024). Avoids decimals when exact. + """ + if num_bytes < 1024: + return f"{num_bytes} bytes" + for factor, unit in ((1 << 30, "GB"), (1 << 20, "MB"), (1 << 10, "KB")): + if num_bytes >= factor: + value = num_bytes / factor + if float(value).is_integer(): + return f"{int(value)} {unit}" + return f"{value:.1f} {unit}" + return f"{num_bytes} bytes" + + +def _validate_file_mounts(file_mounts: Optional[Dict[str, str]] | Omit) -> ValidationNotification: + """Validate file_mounts are within size constraints: returns validation failures. Currently enforces a maximum per-file size to avoid server-side issues with large inline file contents. Also enforces a maximum total size across all file_mounts. """ + note = ValidationNotification() + if file_mounts is omit or file_mounts is None: - return + return note total_size_bytes = 0 for mount_path, content in file_mounts.items(): # Measure size in bytes using UTF-8 encoding since payloads are JSON strings size_bytes = len(content.encode("utf-8")) if size_bytes > FILE_MOUNT_MAX_SIZE_BYTES: - raise ValueError( - f"file_mount '{mount_path}' exceeds maximum size of {FILE_MOUNT_MAX_SIZE_BYTES} bytes. Use object_mounts instead." + over = size_bytes - FILE_MOUNT_MAX_SIZE_BYTES + note.add_error( + f"file_mount '{mount_path}' is {_format_bytes(over)} over the limit " + f"({_format_bytes(size_bytes)} / {_format_bytes(FILE_MOUNT_MAX_SIZE_BYTES)}). Use object_mounts instead." ) total_size_bytes += size_bytes if total_size_bytes > FILE_MOUNT_TOTAL_MAX_SIZE_BYTES: - raise ValueError( - f"total file_mounts size exceeds maximum of {FILE_MOUNT_TOTAL_MAX_SIZE_BYTES} bytes. Use object_mounts instead." + total_over = total_size_bytes - FILE_MOUNT_TOTAL_MAX_SIZE_BYTES + note.add_error( + f"total file_mounts size is {_format_bytes(total_over)} over the limit " + f"({_format_bytes(total_size_bytes)} / {_format_bytes(FILE_MOUNT_TOTAL_MAX_SIZE_BYTES)}). Use object_mounts instead." ) + return note + class BlueprintsResource(SyncAPIResource): @cached_property @@ -172,7 +197,9 @@ def create( idempotency_key: Specify a custom idempotency key for this request """ - _validate_file_mounts(file_mounts) + note = _validate_file_mounts(file_mounts) + if note.has_errors(): + raise ValueError(note.error_message()) return self._post( "/v1/blueprints", @@ -788,7 +815,9 @@ async def create( idempotency_key: Specify a custom idempotency key for this request """ - _validate_file_mounts(file_mounts) + note = _validate_file_mounts(file_mounts) + if note.has_errors(): + raise ValueError(note.error_message()) return await self._post( "/v1/blueprints", diff --git a/tests/api_resources/test_blueprints.py b/tests/api_resources/test_blueprints.py index aec7b4433..631b8d549 100644 --- a/tests/api_resources/test_blueprints.py +++ b/tests/api_resources/test_blueprints.py @@ -112,7 +112,7 @@ def test_streaming_response_create(self, client: Runloop) -> None: def test_create_rejects_large_file_mount(self, client: Runloop) -> None: # 512KB + 1 byte too_large_content = "a" * (512 * 1024 + 1) - with pytest.raises(ValueError, match=r"exceeds maximum size"): + with pytest.raises(ValueError, match=r"over the limit"): client.blueprints.create( name="name", file_mounts={"/tmp/large.txt": too_large_content}, @@ -125,7 +125,7 @@ def test_create_rejects_total_file_mount_size(self, client: Runloop) -> None: content_a = "a" * per_file_max content_b = "b" * per_file_max content_c = "c" * 1 - with pytest.raises(ValueError, match=r"total file_mounts size exceeds maximum"): + with pytest.raises(ValueError, match=r"total file_mounts size .* over the limit"): client.blueprints.create( name="name", file_mounts={ @@ -567,7 +567,7 @@ async def test_streaming_response_create(self, async_client: AsyncRunloop) -> No async def test_create_rejects_large_file_mount(self, async_client: AsyncRunloop) -> None: # 512KB + 1 byte too_large_content = "a" * (512 * 1024 + 1) - with pytest.raises(ValueError, match=r"exceeds maximum size"): + with pytest.raises(ValueError, match=r"over the limit"): await async_client.blueprints.create( name="name", file_mounts={"/tmp/large.txt": too_large_content}, @@ -580,7 +580,7 @@ async def test_create_rejects_total_file_mount_size(self, async_client: AsyncRun content_a = "a" * per_file_max content_b = "b" * per_file_max content_c = "c" * 1 - with pytest.raises(ValueError, match=r"total file_mounts size exceeds maximum"): + with pytest.raises(ValueError, match=r"total file_mounts size .* over the limit"): await async_client.blueprints.create( name="name", file_mounts={ From cccf210ecbc4e397c4916fbfe8f63acde6656a72 Mon Sep 17 00:00:00 2001 From: James Chainey Date: Fri, 31 Oct 2025 13:45:56 -0700 Subject: [PATCH 3/5] fixed broken import --- src/runloop_api_client/_utils/__init__.py | 1 - src/runloop_api_client/_utils/_validation.py | 31 +++++++++++++++++++ .../resources/blueprints.py | 3 +- 3 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 src/runloop_api_client/_utils/_validation.py diff --git a/src/runloop_api_client/_utils/__init__.py b/src/runloop_api_client/_utils/__init__.py index 5541c2cc9..80df24dba 100644 --- a/src/runloop_api_client/_utils/__init__.py +++ b/src/runloop_api_client/_utils/__init__.py @@ -58,7 +58,6 @@ maybe_transform as maybe_transform, async_maybe_transform as async_maybe_transform, ) -from ._validation import ValidationNotification as ValidationNotification from ._reflection import ( function_has_argument as function_has_argument, assert_signatures_in_sync as assert_signatures_in_sync, diff --git a/src/runloop_api_client/_utils/_validation.py b/src/runloop_api_client/_utils/_validation.py new file mode 100644 index 000000000..56ec6f76b --- /dev/null +++ b/src/runloop_api_client/_utils/_validation.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +from typing import List, Optional + + +class ValidationNotification: + """Collects validation errors without raising exceptions. + + This follows the notification pattern: validations append errors, and callers + decide how to react (e.g., surface all messages at once or abort). + """ + + def __init__(self) -> None: + self._errors: List[str] = [] + self._causes: List[Optional[Exception]] = [] + + def add_error(self, message: str, cause: Optional[Exception] = None) -> None: + self._errors.append(message) + self._causes.append(cause) + + def has_errors(self) -> bool: + return len(self._errors) > 0 + + @property + def errors(self) -> List[str]: + # Return a copy to avoid external mutation + return list(self._errors) + + def error_message(self) -> str: + # Join with semicolons to present multiple issues succinctly + return "; ".join(self._errors) diff --git a/src/runloop_api_client/resources/blueprints.py b/src/runloop_api_client/resources/blueprints.py index 86c56b51a..4bb8cbace 100644 --- a/src/runloop_api_client/resources/blueprints.py +++ b/src/runloop_api_client/resources/blueprints.py @@ -15,7 +15,8 @@ blueprint_create_from_inspection_params, ) from .._types import NOT_GIVEN, Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given -from .._utils import maybe_transform, async_maybe_transform, ValidationNotification +from .._utils import maybe_transform, async_maybe_transform +from .._utils._validation import ValidationNotification from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( From 5266abbb4e83753f59009b82256dbc9439ef6409 Mon Sep 17 00:00:00 2001 From: James Chainey Date: Mon, 3 Nov 2025 14:51:15 -0800 Subject: [PATCH 4/5] made file_mount limits more restrictive --- src/runloop_api_client/_constants.py | 7 +++-- tests/api_resources/test_blueprints.py | 38 ++++++++++---------------- 2 files changed, 19 insertions(+), 26 deletions(-) diff --git a/src/runloop_api_client/_constants.py b/src/runloop_api_client/_constants.py index e03a0d7a6..956e12ce0 100644 --- a/src/runloop_api_client/_constants.py +++ b/src/runloop_api_client/_constants.py @@ -14,7 +14,10 @@ MAX_RETRY_DELAY = 60.0 # Maximum allowed size (in bytes) for individual entries in `file_mounts` when creating Blueprints -FILE_MOUNT_MAX_SIZE_BYTES = 512 * 1024 +# NOTE: Capped at ~786,000 bytes to align with the approximate macOS maximum +# shell command length when embedding base64-encoded content. Since base64 is +# ASCII, bytes ≈ characters here, and we measure size using UTF-8 encoding. +FILE_MOUNT_MAX_SIZE_BYTES = 786_000 # Maximum allowed total size (in bytes) across all `file_mounts` when creating Blueprints -FILE_MOUNT_TOTAL_MAX_SIZE_BYTES = 1024 * 1024 +FILE_MOUNT_TOTAL_MAX_SIZE_BYTES = 786_000 * 10 # ~10 mb diff --git a/tests/api_resources/test_blueprints.py b/tests/api_resources/test_blueprints.py index 631b8d549..5643f5da4 100644 --- a/tests/api_resources/test_blueprints.py +++ b/tests/api_resources/test_blueprints.py @@ -110,8 +110,8 @@ def test_streaming_response_create(self, client: Runloop) -> None: @parametrize def test_create_rejects_large_file_mount(self, client: Runloop) -> None: - # 512KB + 1 byte - too_large_content = "a" * (512 * 1024 + 1) + # 786,000 bytes + 1 byte + too_large_content = "a" * (786_000 + 1) with pytest.raises(ValueError, match=r"over the limit"): client.blueprints.create( name="name", @@ -120,19 +120,14 @@ def test_create_rejects_large_file_mount(self, client: Runloop) -> None: @parametrize def test_create_rejects_total_file_mount_size(self, client: Runloop) -> None: - # Two files at exactly per-file max, plus 1 extra byte across a third file to exceed 1MB total - per_file_max = 512 * 1024 - content_a = "a" * per_file_max - content_b = "b" * per_file_max - content_c = "c" * 1 + # Ten files at exactly per-file max, plus 1 extra byte to exceed total limit + per_file_max = 786_000 + file_mounts = {f"/tmp/{i}.txt": "a" * per_file_max for i in range(10)} + file_mounts["/tmp/extra.txt"] = "x" with pytest.raises(ValueError, match=r"total file_mounts size .* over the limit"): client.blueprints.create( name="name", - file_mounts={ - "/tmp/a.txt": content_a, - "/tmp/b.txt": content_b, - "/tmp/c.txt": content_c, - }, + file_mounts=file_mounts, ) @parametrize @@ -565,8 +560,8 @@ async def test_streaming_response_create(self, async_client: AsyncRunloop) -> No @parametrize async def test_create_rejects_large_file_mount(self, async_client: AsyncRunloop) -> None: - # 512KB + 1 byte - too_large_content = "a" * (512 * 1024 + 1) + # 786,000 bytes + 1 byte + too_large_content = "a" * (786_000 + 1) with pytest.raises(ValueError, match=r"over the limit"): await async_client.blueprints.create( name="name", @@ -575,19 +570,14 @@ async def test_create_rejects_large_file_mount(self, async_client: AsyncRunloop) @parametrize async def test_create_rejects_total_file_mount_size(self, async_client: AsyncRunloop) -> None: - # Two files at exactly per-file max, plus 1 extra byte across a third file to exceed 1MB total - per_file_max = 512 * 1024 - content_a = "a" * per_file_max - content_b = "b" * per_file_max - content_c = "c" * 1 + # Ten files at exactly per-file max, plus 1 extra byte to exceed total limit + per_file_max = 786_000 + file_mounts = {f"/tmp/{i}.txt": "a" * per_file_max for i in range(10)} + file_mounts["/tmp/extra.txt"] = "x" with pytest.raises(ValueError, match=r"total file_mounts size .* over the limit"): await async_client.blueprints.create( name="name", - file_mounts={ - "/tmp/a.txt": content_a, - "/tmp/b.txt": content_b, - "/tmp/c.txt": content_c, - }, + file_mounts=file_mounts, ) @parametrize From 640c4e9af81fa4071f2916c205530ed1d05ff6fc Mon Sep 17 00:00:00 2001 From: James Chainey Date: Wed, 5 Nov 2025 10:38:06 -0800 Subject: [PATCH 5/5] updated with actual tested limits --- src/runloop_api_client/_constants.py | 8 ++++---- tests/api_resources/test_blueprints.py | 20 ++++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/runloop_api_client/_constants.py b/src/runloop_api_client/_constants.py index 956e12ce0..d6361c8ad 100644 --- a/src/runloop_api_client/_constants.py +++ b/src/runloop_api_client/_constants.py @@ -14,10 +14,10 @@ MAX_RETRY_DELAY = 60.0 # Maximum allowed size (in bytes) for individual entries in `file_mounts` when creating Blueprints -# NOTE: Capped at ~786,000 bytes to align with the approximate macOS maximum -# shell command length when embedding base64-encoded content. Since base64 is -# ASCII, bytes ≈ characters here, and we measure size using UTF-8 encoding. -FILE_MOUNT_MAX_SIZE_BYTES = 786_000 +# NOTE: Empirically, ~131,000 is the maximum command length after +# base64 encoding; 98,250 is the pre-encoded limit that stays within that bound. +# We measure size in bytes using UTF-8 encoding; base64 output is ASCII. +FILE_MOUNT_MAX_SIZE_BYTES = 98_250 # Maximum allowed total size (in bytes) across all `file_mounts` when creating Blueprints FILE_MOUNT_TOTAL_MAX_SIZE_BYTES = 786_000 * 10 # ~10 mb diff --git a/tests/api_resources/test_blueprints.py b/tests/api_resources/test_blueprints.py index 5643f5da4..9826f0b67 100644 --- a/tests/api_resources/test_blueprints.py +++ b/tests/api_resources/test_blueprints.py @@ -110,8 +110,8 @@ def test_streaming_response_create(self, client: Runloop) -> None: @parametrize def test_create_rejects_large_file_mount(self, client: Runloop) -> None: - # 786,000 bytes + 1 byte - too_large_content = "a" * (786_000 + 1) + # 98,250 bytes + 1 byte (pre-encoded limit to stay within ~131,000 b64'd) + too_large_content = "a" * (98_250 + 1) with pytest.raises(ValueError, match=r"over the limit"): client.blueprints.create( name="name", @@ -120,9 +120,9 @@ def test_create_rejects_large_file_mount(self, client: Runloop) -> None: @parametrize def test_create_rejects_total_file_mount_size(self, client: Runloop) -> None: - # Ten files at exactly per-file max, plus 1 extra byte to exceed total limit - per_file_max = 786_000 - file_mounts = {f"/tmp/{i}.txt": "a" * per_file_max for i in range(10)} + # Eighty files at per-file max (98,250) equals current total limit; add 1 byte to exceed + per_file_max = 98_250 + file_mounts = {f"/tmp/{i}.txt": "a" * per_file_max for i in range(80)} file_mounts["/tmp/extra.txt"] = "x" with pytest.raises(ValueError, match=r"total file_mounts size .* over the limit"): client.blueprints.create( @@ -560,8 +560,8 @@ async def test_streaming_response_create(self, async_client: AsyncRunloop) -> No @parametrize async def test_create_rejects_large_file_mount(self, async_client: AsyncRunloop) -> None: - # 786,000 bytes + 1 byte - too_large_content = "a" * (786_000 + 1) + # 98,250 bytes + 1 byte (pre-encoded limit to stay within ~131,000 b64'd) + too_large_content = "a" * (98_250 + 1) with pytest.raises(ValueError, match=r"over the limit"): await async_client.blueprints.create( name="name", @@ -570,9 +570,9 @@ async def test_create_rejects_large_file_mount(self, async_client: AsyncRunloop) @parametrize async def test_create_rejects_total_file_mount_size(self, async_client: AsyncRunloop) -> None: - # Ten files at exactly per-file max, plus 1 extra byte to exceed total limit - per_file_max = 786_000 - file_mounts = {f"/tmp/{i}.txt": "a" * per_file_max for i in range(10)} + # Eighty files at per-file max (98,250) equals current total limit; add 1 byte to exceed + per_file_max = 98_250 + file_mounts = {f"/tmp/{i}.txt": "a" * per_file_max for i in range(80)} file_mounts["/tmp/extra.txt"] = "x" with pytest.raises(ValueError, match=r"total file_mounts size .* over the limit"): await async_client.blueprints.create(