modal 1.1.5.dev83__py3-none-any.whl → 1.3.1.dev8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of modal might be problematic. Click here for more details.

Files changed (139) hide show
  1. modal/__init__.py +4 -4
  2. modal/__main__.py +4 -29
  3. modal/_billing.py +84 -0
  4. modal/_clustered_functions.py +1 -3
  5. modal/_container_entrypoint.py +33 -208
  6. modal/_functions.py +146 -121
  7. modal/_grpc_client.py +191 -0
  8. modal/_ipython.py +16 -6
  9. modal/_load_context.py +106 -0
  10. modal/_object.py +72 -21
  11. modal/_output.py +12 -14
  12. modal/_partial_function.py +31 -4
  13. modal/_resolver.py +44 -57
  14. modal/_runtime/container_io_manager.py +26 -28
  15. modal/_runtime/container_io_manager.pyi +42 -44
  16. modal/_runtime/gpu_memory_snapshot.py +9 -7
  17. modal/_runtime/user_code_event_loop.py +80 -0
  18. modal/_runtime/user_code_imports.py +236 -10
  19. modal/_serialization.py +2 -1
  20. modal/_traceback.py +4 -13
  21. modal/_tunnel.py +16 -11
  22. modal/_tunnel.pyi +25 -3
  23. modal/_utils/async_utils.py +337 -10
  24. modal/_utils/auth_token_manager.py +1 -4
  25. modal/_utils/blob_utils.py +29 -22
  26. modal/_utils/function_utils.py +20 -21
  27. modal/_utils/grpc_testing.py +6 -3
  28. modal/_utils/grpc_utils.py +223 -64
  29. modal/_utils/mount_utils.py +26 -1
  30. modal/_utils/package_utils.py +0 -1
  31. modal/_utils/rand_pb_testing.py +8 -1
  32. modal/_utils/task_command_router_client.py +524 -0
  33. modal/_vendor/cloudpickle.py +144 -48
  34. modal/app.py +215 -96
  35. modal/app.pyi +78 -37
  36. modal/billing.py +5 -0
  37. modal/builder/2025.06.txt +6 -3
  38. modal/builder/PREVIEW.txt +2 -1
  39. modal/builder/base-images.json +4 -2
  40. modal/cli/_download.py +19 -3
  41. modal/cli/cluster.py +4 -2
  42. modal/cli/config.py +3 -1
  43. modal/cli/container.py +5 -4
  44. modal/cli/dict.py +5 -2
  45. modal/cli/entry_point.py +26 -2
  46. modal/cli/environment.py +2 -16
  47. modal/cli/launch.py +1 -76
  48. modal/cli/network_file_system.py +5 -20
  49. modal/cli/queues.py +5 -4
  50. modal/cli/run.py +24 -204
  51. modal/cli/secret.py +1 -2
  52. modal/cli/shell.py +375 -0
  53. modal/cli/utils.py +1 -13
  54. modal/cli/volume.py +11 -17
  55. modal/client.py +16 -125
  56. modal/client.pyi +94 -144
  57. modal/cloud_bucket_mount.py +3 -1
  58. modal/cloud_bucket_mount.pyi +4 -0
  59. modal/cls.py +101 -64
  60. modal/cls.pyi +9 -8
  61. modal/config.py +21 -1
  62. modal/container_process.py +288 -12
  63. modal/container_process.pyi +99 -38
  64. modal/dict.py +72 -33
  65. modal/dict.pyi +88 -57
  66. modal/environments.py +16 -8
  67. modal/environments.pyi +6 -2
  68. modal/exception.py +154 -16
  69. modal/experimental/__init__.py +23 -5
  70. modal/experimental/flash.py +161 -74
  71. modal/experimental/flash.pyi +97 -49
  72. modal/file_io.py +50 -92
  73. modal/file_io.pyi +117 -89
  74. modal/functions.pyi +70 -87
  75. modal/image.py +73 -47
  76. modal/image.pyi +33 -30
  77. modal/io_streams.py +500 -149
  78. modal/io_streams.pyi +279 -189
  79. modal/mount.py +60 -45
  80. modal/mount.pyi +41 -17
  81. modal/network_file_system.py +19 -11
  82. modal/network_file_system.pyi +72 -39
  83. modal/object.pyi +114 -22
  84. modal/parallel_map.py +42 -44
  85. modal/parallel_map.pyi +9 -17
  86. modal/partial_function.pyi +4 -2
  87. modal/proxy.py +14 -6
  88. modal/proxy.pyi +10 -2
  89. modal/queue.py +45 -38
  90. modal/queue.pyi +88 -52
  91. modal/runner.py +96 -96
  92. modal/runner.pyi +44 -27
  93. modal/sandbox.py +225 -108
  94. modal/sandbox.pyi +226 -63
  95. modal/secret.py +58 -56
  96. modal/secret.pyi +28 -13
  97. modal/serving.py +7 -11
  98. modal/serving.pyi +7 -8
  99. modal/snapshot.py +29 -15
  100. modal/snapshot.pyi +18 -10
  101. modal/token_flow.py +1 -1
  102. modal/token_flow.pyi +4 -6
  103. modal/volume.py +102 -55
  104. modal/volume.pyi +125 -66
  105. {modal-1.1.5.dev83.dist-info → modal-1.3.1.dev8.dist-info}/METADATA +10 -9
  106. modal-1.3.1.dev8.dist-info/RECORD +189 -0
  107. modal_proto/api.proto +86 -30
  108. modal_proto/api_grpc.py +10 -25
  109. modal_proto/api_pb2.py +1080 -1047
  110. modal_proto/api_pb2.pyi +253 -79
  111. modal_proto/api_pb2_grpc.py +14 -48
  112. modal_proto/api_pb2_grpc.pyi +6 -18
  113. modal_proto/modal_api_grpc.py +175 -176
  114. modal_proto/{sandbox_router.proto → task_command_router.proto} +62 -45
  115. modal_proto/task_command_router_grpc.py +138 -0
  116. modal_proto/task_command_router_pb2.py +180 -0
  117. modal_proto/{sandbox_router_pb2.pyi → task_command_router_pb2.pyi} +110 -63
  118. modal_proto/task_command_router_pb2_grpc.py +272 -0
  119. modal_proto/task_command_router_pb2_grpc.pyi +100 -0
  120. modal_version/__init__.py +1 -1
  121. modal_version/__main__.py +1 -1
  122. modal/cli/programs/launch_instance_ssh.py +0 -94
  123. modal/cli/programs/run_marimo.py +0 -95
  124. modal-1.1.5.dev83.dist-info/RECORD +0 -191
  125. modal_proto/modal_options_grpc.py +0 -3
  126. modal_proto/options.proto +0 -19
  127. modal_proto/options_grpc.py +0 -3
  128. modal_proto/options_pb2.py +0 -35
  129. modal_proto/options_pb2.pyi +0 -20
  130. modal_proto/options_pb2_grpc.py +0 -4
  131. modal_proto/options_pb2_grpc.pyi +0 -7
  132. modal_proto/sandbox_router_grpc.py +0 -105
  133. modal_proto/sandbox_router_pb2.py +0 -148
  134. modal_proto/sandbox_router_pb2_grpc.py +0 -203
  135. modal_proto/sandbox_router_pb2_grpc.pyi +0 -75
  136. {modal-1.1.5.dev83.dist-info → modal-1.3.1.dev8.dist-info}/WHEEL +0 -0
  137. {modal-1.1.5.dev83.dist-info → modal-1.3.1.dev8.dist-info}/entry_points.txt +0 -0
  138. {modal-1.1.5.dev83.dist-info → modal-1.3.1.dev8.dist-info}/licenses/LICENSE +0 -0
  139. {modal-1.1.5.dev83.dist-info → modal-1.3.1.dev8.dist-info}/top_level.txt +0 -0
modal/_functions.py CHANGED
@@ -13,13 +13,14 @@ from typing import TYPE_CHECKING, Any, AsyncIterator, Callable, Optional, Union
13
13
 
14
14
  import typing_extensions
15
15
  from google.protobuf.message import Message
16
- from grpclib import GRPCError, Status
16
+ from grpclib import Status
17
17
  from synchronicity.combined_types import MethodWithAio
18
18
 
19
19
  from modal_proto import api_pb2
20
20
  from modal_proto.modal_api_grpc import ModalClientModal
21
21
 
22
- from ._object import _get_environment_name, _Object, live_method, live_method_gen
22
+ from ._load_context import LoadContext
23
+ from ._object import _Object, live_method, live_method_gen
23
24
  from ._pty import get_pty_info
24
25
  from ._resolver import Resolver
25
26
  from ._resources import convert_fn_config_to_resources_config
@@ -37,6 +38,7 @@ from ._utils.async_utils import (
37
38
  aclosing,
38
39
  async_merge,
39
40
  callable_to_agen,
41
+ deprecate_aio_usage,
40
42
  synchronizer,
41
43
  warn_if_generator_is_not_consumed,
42
44
  )
@@ -53,7 +55,7 @@ from ._utils.function_utils import (
53
55
  get_function_type,
54
56
  is_async,
55
57
  )
56
- from ._utils.grpc_utils import RetryWarningMessage, retry_transient_errors
58
+ from ._utils.grpc_utils import Retry, RetryWarningMessage
57
59
  from ._utils.mount_utils import validate_network_file_systems, validate_volumes
58
60
  from .call_graph import InputInfo, _reconstruct_call_graph
59
61
  from .client import _Client
@@ -89,13 +91,13 @@ from .parallel_map import (
89
91
  from .proxy import _Proxy
90
92
  from .retries import Retries, RetryManager
91
93
  from .schedule import Schedule
92
- from .scheduler_placement import SchedulerPlacement
93
94
  from .secret import _Secret
94
95
  from .volume import _Volume
95
96
 
96
97
  if TYPE_CHECKING:
97
98
  import modal.app
98
99
  import modal.cls
100
+ import modal.functions
99
101
 
100
102
  MAX_INTERNAL_FAILURE_COUNT = 8
101
103
  TERMINAL_STATUSES = (
@@ -164,21 +166,22 @@ class _Invocation:
164
166
 
165
167
  if from_spawn_map:
166
168
  request.from_spawn_map = True
167
- response = await retry_transient_errors(
168
- client.stub.FunctionMap,
169
+ response = await client.stub.FunctionMap(
169
170
  request,
170
- max_retries=None,
171
- max_delay=30.0,
172
- retry_warning_message=RetryWarningMessage(
173
- message="Warning: `.spawn_map(...)` for function `{self._function_name}` is waiting to create"
174
- "more function calls. This may be due to hitting rate limits or function backlog limits.",
175
- warning_interval=10,
176
- errors_to_warn_for=[Status.RESOURCE_EXHAUSTED],
171
+ retry=Retry(
172
+ max_retries=None,
173
+ max_delay=30.0,
174
+ warning_message=RetryWarningMessage(
175
+ message="Warning: `.spawn_map(...)` for function `{self._function_name}` is waiting to create"
176
+ "more function calls. This may be due to hitting rate limits or function backlog limits.",
177
+ warning_interval=10,
178
+ errors_to_warn_for=[Status.RESOURCE_EXHAUSTED],
179
+ ),
180
+ additional_status_codes=[Status.RESOURCE_EXHAUSTED],
177
181
  ),
178
- additional_status_codes=[Status.RESOURCE_EXHAUSTED],
179
182
  )
180
183
  else:
181
- response = await retry_transient_errors(client.stub.FunctionMap, request)
184
+ response = await client.stub.FunctionMap(request)
182
185
 
183
186
  function_call_id = response.function_call_id
184
187
  if response.pipelined_inputs:
@@ -198,10 +201,7 @@ class _Invocation:
198
201
  request_put = api_pb2.FunctionPutInputsRequest(
199
202
  function_id=function_id, inputs=[item], function_call_id=function_call_id
200
203
  )
201
- inputs_response: api_pb2.FunctionPutInputsResponse = await retry_transient_errors(
202
- client.stub.FunctionPutInputs,
203
- request_put,
204
- )
204
+ inputs_response: api_pb2.FunctionPutInputsResponse = await client.stub.FunctionPutInputs(request_put)
205
205
  processed_inputs = inputs_response.inputs
206
206
  if not processed_inputs:
207
207
  raise Exception("Could not create function call - the input queue seems to be full")
@@ -243,10 +243,9 @@ class _Invocation:
243
243
  start_idx=index,
244
244
  end_idx=index,
245
245
  )
246
- response: api_pb2.FunctionGetOutputsResponse = await retry_transient_errors(
247
- self.stub.FunctionGetOutputs,
246
+ response: api_pb2.FunctionGetOutputsResponse = await self.stub.FunctionGetOutputs(
248
247
  request,
249
- attempt_timeout=backend_timeout + ATTEMPT_TIMEOUT_GRACE_PERIOD,
248
+ retry=Retry(attempt_timeout=backend_timeout + ATTEMPT_TIMEOUT_GRACE_PERIOD),
250
249
  )
251
250
 
252
251
  if len(response.outputs) > 0:
@@ -266,10 +265,7 @@ class _Invocation:
266
265
 
267
266
  item = api_pb2.FunctionRetryInputsItem(input_jwt=ctx.input_jwt, input=ctx.item.input)
268
267
  request = api_pb2.FunctionRetryInputsRequest(function_call_jwt=ctx.function_call_jwt, inputs=[item])
269
- await retry_transient_errors(
270
- self.stub.FunctionRetryInputs,
271
- request,
272
- )
268
+ await self.stub.FunctionRetryInputs(request)
273
269
 
274
270
  async def _get_single_output(self, expected_jwt: Optional[str] = None) -> api_pb2.FunctionGetOutputsItem:
275
271
  # waits indefinitely for a single result for the function, and clear the outputs buffer after
@@ -373,10 +369,8 @@ class _Invocation:
373
369
  start_idx=current_index,
374
370
  end_idx=batch_end_index,
375
371
  )
376
- response: api_pb2.FunctionGetOutputsResponse = await retry_transient_errors(
377
- self.stub.FunctionGetOutputs,
378
- request,
379
- attempt_timeout=ATTEMPT_TIMEOUT_GRACE_PERIOD,
372
+ response: api_pb2.FunctionGetOutputsResponse = await self.stub.FunctionGetOutputs(
373
+ request, retry=Retry(attempt_timeout=ATTEMPT_TIMEOUT_GRACE_PERIOD)
380
374
  )
381
375
 
382
376
  outputs = list(response.outputs)
@@ -448,7 +442,7 @@ class _InputPlaneInvocation:
448
442
  )
449
443
 
450
444
  metadata = await client.get_input_plane_metadata(input_plane_region)
451
- response = await retry_transient_errors(stub.AttemptStart, request, metadata=metadata)
445
+ response = await stub.AttemptStart(request, metadata=metadata)
452
446
  attempt_token = response.attempt_token
453
447
 
454
448
  return _InputPlaneInvocation(
@@ -468,10 +462,9 @@ class _InputPlaneInvocation:
468
462
  requested_at=time.time(),
469
463
  )
470
464
  metadata = await self.client.get_input_plane_metadata(self.input_plane_region)
471
- await_response: api_pb2.AttemptAwaitResponse = await retry_transient_errors(
472
- self.stub.AttemptAwait,
465
+ await_response: api_pb2.AttemptAwaitResponse = await self.stub.AttemptAwait(
473
466
  await_request,
474
- attempt_timeout=OUTPUTS_TIMEOUT + ATTEMPT_TIMEOUT_GRACE_PERIOD,
467
+ retry=Retry(attempt_timeout=OUTPUTS_TIMEOUT + ATTEMPT_TIMEOUT_GRACE_PERIOD),
475
468
  metadata=metadata,
476
469
  )
477
470
 
@@ -511,11 +504,7 @@ class _InputPlaneInvocation:
511
504
  input=self.input_item,
512
505
  attempt_token=self.attempt_token,
513
506
  )
514
- retry_response = await retry_transient_errors(
515
- self.stub.AttemptRetry,
516
- retry_request,
517
- metadata=metadata,
518
- )
507
+ retry_response = await self.stub.AttemptRetry(retry_request, metadata=metadata)
519
508
  return retry_response.attempt_token
520
509
 
521
510
  async def run_generator(self):
@@ -549,6 +538,7 @@ class _InputPlaneInvocation:
549
538
  async def _get_metadata(input_plane_region: str, client: _Client) -> list[tuple[str, str]]:
550
539
  if not input_plane_region:
551
540
  return []
541
+ assert client._auth_token_manager, "Client is not open"
552
542
  token = await client._auth_token_manager.get_token()
553
543
  return [("x-modal-input-plane-region", input_plane_region), ("x-modal-auth-token", token)]
554
544
 
@@ -601,7 +591,7 @@ class _FunctionSpec:
601
591
  cpu: Optional[Union[float, tuple[float, float]]]
602
592
  memory: Optional[Union[int, tuple[int, int]]]
603
593
  ephemeral_disk: Optional[int]
604
- scheduler_placement: Optional[SchedulerPlacement]
594
+ scheduler_placement: Optional[api_pb2.SchedulerPlacement]
605
595
  proxy: Optional[_Proxy]
606
596
 
607
597
 
@@ -669,7 +659,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
669
659
  @staticmethod
670
660
  def from_local(
671
661
  info: FunctionInfo,
672
- app,
662
+ app: Optional["modal.app._App"], # App here should only be None in case of Image.run_function
673
663
  image: _Image,
674
664
  env: Optional[dict[str, Optional[str]]] = None,
675
665
  secrets: Optional[Collection[_Secret]] = None,
@@ -695,7 +685,8 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
695
685
  batch_max_size: Optional[int] = None,
696
686
  batch_wait_ms: Optional[int] = None,
697
687
  cloud: Optional[str] = None,
698
- scheduler_placement: Optional[SchedulerPlacement] = None,
688
+ region: Optional[Union[str, Sequence[str]]] = None,
689
+ nonpreemptible: bool = False,
699
690
  is_builder_function: bool = False,
700
691
  is_auto_snapshot: bool = False,
701
692
  enable_memory_snapshot: bool = False,
@@ -705,13 +696,14 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
705
696
  # Experimental: Clustered functions
706
697
  cluster_size: Optional[int] = None,
707
698
  rdma: Optional[bool] = None,
708
- max_inputs: Optional[int] = None,
699
+ single_use_containers: bool = False,
709
700
  ephemeral_disk: Optional[int] = None,
710
701
  include_source: bool = True,
711
702
  experimental_options: Optional[dict[str, str]] = None,
712
703
  _experimental_proxy_ip: Optional[str] = None,
713
704
  _experimental_custom_scaling_factor: Optional[float] = None,
714
705
  restrict_output: bool = False,
706
+ http_config: Optional[api_pb2.HTTPConfig] = None,
715
707
  ) -> "_Function":
716
708
  """mdmd:hidden
717
709
 
@@ -758,6 +750,11 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
758
750
  if env:
759
751
  secrets = [*secrets, _Secret.from_dict(env)]
760
752
 
753
+ scheduler_placement: Optional[api_pb2.SchedulerPlacement] = None
754
+ if region or nonpreemptible:
755
+ regions = [region] if isinstance(region, str) else (list(region) if region else None)
756
+ scheduler_placement = api_pb2.SchedulerPlacement(regions=regions, nonpreemptible=nonpreemptible)
757
+
761
758
  function_spec = _FunctionSpec(
762
759
  mounts=all_mounts,
763
760
  secrets=secrets,
@@ -790,6 +787,16 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
790
787
  scaledown_window=scaledown_window,
791
788
  )
792
789
 
790
+ # For clustered functions, container settings must be multiples of cluster_size
791
+ if cluster_size is not None and cluster_size > 1:
792
+ for field in ["min_containers", "max_containers", "buffer_containers"]:
793
+ value = getattr(autoscaler_settings, field)
794
+ if value and value % cluster_size != 0:
795
+ raise InvalidError(
796
+ f"`{field}` ({value}) must be a multiple of `cluster_size` ({cluster_size}) "
797
+ f"for clustered Functions"
798
+ )
799
+
793
800
  if _experimental_custom_scaling_factor is not None and (
794
801
  _experimental_custom_scaling_factor < 0 or _experimental_custom_scaling_factor > 1
795
802
  ):
@@ -815,14 +822,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
815
822
  if arg.default is not inspect.Parameter.empty:
816
823
  raise InvalidError(f"Modal batched function {func_name} does not accept default arguments.")
817
824
 
818
- if max_inputs is not None:
819
- if not isinstance(max_inputs, int):
820
- raise InvalidError(f"`max_inputs` must be an int, not {type(max_inputs).__name__}")
821
- if max_inputs <= 0:
822
- raise InvalidError("`max_inputs` must be positive")
823
- if max_inputs > 1:
824
- raise InvalidError("Only `max_inputs=1` is currently supported")
825
-
826
825
  # Validate volumes
827
826
  validated_volumes = validate_volumes(volumes)
828
827
  cloud_bucket_mounts = [(k, v) for k, v in validated_volumes if isinstance(v, _CloudBucketMount)]
@@ -895,12 +894,12 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
895
894
  is_web_endpoint, is_generator, restrict_output
896
895
  )
897
896
 
898
- async def _preload(self: _Function, resolver: Resolver, existing_object_id: Optional[str]):
899
- assert resolver.client and resolver.client.stub
900
-
901
- assert resolver.app_id
897
+ async def _preload(
898
+ self: _Function, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
899
+ ):
900
+ assert load_context.app_id
902
901
  req = api_pb2.FunctionPrecreateRequest(
903
- app_id=resolver.app_id,
902
+ app_id=load_context.app_id,
904
903
  function_name=info.function_name,
905
904
  function_type=function_type,
906
905
  existing_function_id=existing_object_id or "",
@@ -916,11 +915,12 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
916
915
  elif webhook_config:
917
916
  req.webhook_config.CopyFrom(webhook_config)
918
917
 
919
- response = await retry_transient_errors(resolver.client.stub.FunctionPrecreate, req)
920
- self._hydrate(response.function_id, resolver.client, response.handle_metadata)
918
+ response = await load_context.client.stub.FunctionPrecreate(req)
919
+ self._hydrate(response.function_id, load_context.client, response.handle_metadata)
921
920
 
922
- async def _load(self: _Function, resolver: Resolver, existing_object_id: Optional[str]):
923
- assert resolver.client and resolver.client.stub
921
+ async def _load(
922
+ self: _Function, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
923
+ ):
924
924
  with FunctionCreationStatus(resolver, tag) as function_creation_status:
925
925
  timeout_secs = timeout
926
926
 
@@ -992,6 +992,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
992
992
  function_definition = api_pb2.Function(
993
993
  module_name=info.module_name or "",
994
994
  function_name=info.function_name,
995
+ implementation_name=info.implementation_name,
995
996
  mount_ids=loaded_mount_ids,
996
997
  secret_ids=[secret.object_id for secret in secrets],
997
998
  image_id=(image.object_id if image else ""),
@@ -1027,9 +1028,10 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1027
1028
  object_dependencies=object_dependencies,
1028
1029
  block_network=block_network,
1029
1030
  untrusted=restrict_modal_access,
1030
- max_inputs=max_inputs or 0,
1031
+ single_use_containers=single_use_containers,
1032
+ max_inputs=int(single_use_containers), # TODO(michael) remove after worker rollover
1031
1033
  cloud_bucket_mounts=cloud_bucket_mounts_to_proto(cloud_bucket_mounts),
1032
- scheduler_placement=scheduler_placement.proto if scheduler_placement else None,
1034
+ scheduler_placement=scheduler_placement,
1033
1035
  is_class=info.is_service_class(),
1034
1036
  class_parameter_info=info.class_parameter_info(),
1035
1037
  i6pn_enabled=i6pn_enabled,
@@ -1051,12 +1053,14 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1051
1053
  function_schema=function_schema,
1052
1054
  supported_input_formats=supported_input_formats,
1053
1055
  supported_output_formats=supported_output_formats,
1056
+ http_config=http_config,
1054
1057
  )
1055
1058
 
1056
1059
  if isinstance(gpu, list):
1057
1060
  function_data = api_pb2.FunctionData(
1058
1061
  module_name=function_definition.module_name,
1059
1062
  function_name=function_definition.function_name,
1063
+ implementation_name=function_definition.implementation_name,
1060
1064
  function_type=function_definition.function_type,
1061
1065
  warm_pool_size=function_definition.warm_pool_size,
1062
1066
  concurrency_limit=function_definition.concurrency_limit,
@@ -1088,6 +1092,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1088
1092
  untrusted=function_definition.untrusted,
1089
1093
  supported_input_formats=supported_input_formats,
1090
1094
  supported_output_formats=supported_output_formats,
1095
+ http_config=http_config,
1091
1096
  )
1092
1097
 
1093
1098
  ranked_functions = []
@@ -1116,24 +1121,18 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1116
1121
  ),
1117
1122
  )
1118
1123
 
1119
- assert resolver.app_id
1124
+ assert load_context.app_id
1120
1125
  assert (function_definition is None) != (function_data is None) # xor
1121
1126
  request = api_pb2.FunctionCreateRequest(
1122
- app_id=resolver.app_id,
1127
+ app_id=load_context.app_id,
1123
1128
  function=function_definition,
1124
1129
  function_data=function_data,
1125
1130
  existing_function_id=existing_object_id or "",
1126
1131
  )
1127
1132
  try:
1128
- response: api_pb2.FunctionCreateResponse = await retry_transient_errors(
1129
- resolver.client.stub.FunctionCreate, request
1130
- )
1131
- except GRPCError as exc:
1132
- if exc.status == Status.INVALID_ARGUMENT:
1133
- raise InvalidError(exc.message)
1134
- if exc.status == Status.FAILED_PRECONDITION:
1135
- raise InvalidError(exc.message)
1136
- if exc.message and "Received :status = '413'" in exc.message:
1133
+ response: api_pb2.FunctionCreateResponse = await load_context.client.stub.FunctionCreate(request)
1134
+ except Exception as exc:
1135
+ if "Received :status = '413'" in str(exc):
1137
1136
  raise InvalidError(f"Function {info.function_name} is too large to deploy.")
1138
1137
  raise
1139
1138
  function_creation_status.set_response(response)
@@ -1142,10 +1141,14 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1142
1141
  serve_mounts = {m for m in all_mounts if m.is_local()}
1143
1142
  serve_mounts |= image._serve_mounts
1144
1143
  obj._serve_mounts = frozenset(serve_mounts)
1145
- self._hydrate(response.function_id, resolver.client, response.handle_metadata)
1144
+ self._hydrate(response.function_id, load_context.client, response.handle_metadata)
1146
1145
 
1147
1146
  rep = f"Function({tag})"
1148
- obj = _Function._from_loader(_load, rep, preload=_preload, deps=_deps)
1147
+ # Pass a *reference* to the App's LoadContext - this is important since the App is
1148
+ # the only way to infer a LoadContext for an `@app.function`, and the App doesn't
1149
+ # get its client until *after* the Function is created.
1150
+ load_context = app._root_load_context if app else LoadContext.empty()
1151
+ obj = _Function._from_loader(_load, rep, preload=_preload, deps=_deps, load_context_overrides=load_context)
1149
1152
 
1150
1153
  obj._raw_f = info.raw_f
1151
1154
  obj._info = info
@@ -1187,7 +1190,12 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1187
1190
 
1188
1191
  parent = self
1189
1192
 
1190
- async def _load(param_bound_func: _Function, resolver: Resolver, existing_object_id: Optional[str]):
1193
+ async def _load(
1194
+ param_bound_func: _Function,
1195
+ resolver: Resolver,
1196
+ load_context: LoadContext,
1197
+ existing_object_id: Optional[str],
1198
+ ):
1191
1199
  if not parent.is_hydrated:
1192
1200
  # While the base Object.hydrate() method appears to be idempotent, it's not always safe
1193
1201
  await parent.hydrate()
@@ -1220,7 +1228,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1220
1228
  param_bound_func._hydrate_from_other(parent)
1221
1229
  return
1222
1230
 
1223
- environment_name = _get_environment_name(None, resolver)
1224
1231
  assert parent is not None and parent.is_hydrated
1225
1232
 
1226
1233
  if options:
@@ -1260,11 +1267,11 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1260
1267
  function_id=parent.object_id,
1261
1268
  serialized_params=serialized_params,
1262
1269
  function_options=options_pb,
1263
- environment_name=environment_name
1270
+ environment_name=load_context.environment_name
1264
1271
  or "", # TODO: investigate shouldn't environment name always be specified here?
1265
1272
  )
1266
1273
 
1267
- response = await retry_transient_errors(parent._client.stub.FunctionBindParams, req)
1274
+ response = await parent._client.stub.FunctionBindParams(req)
1268
1275
  param_bound_func._hydrate(response.bound_function_id, parent._client, response.handle_metadata)
1269
1276
 
1270
1277
  def _deps():
@@ -1277,7 +1284,13 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1277
1284
  return [dep for dep in all_deps if not dep.is_hydrated]
1278
1285
  return []
1279
1286
 
1280
- fun: _Function = _Function._from_loader(_load, "Function(parametrized)", hydrate_lazily=True, deps=_deps)
1287
+ fun: _Function = _Function._from_loader(
1288
+ _load,
1289
+ "Function(parametrized)",
1290
+ hydrate_lazily=True,
1291
+ deps=_deps,
1292
+ load_context_overrides=self._load_context_overrides,
1293
+ )
1281
1294
 
1282
1295
  fun._info = self._info
1283
1296
  fun._obj = obj
@@ -1328,7 +1341,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1328
1341
  scaledown_window=scaledown_window,
1329
1342
  )
1330
1343
  request = api_pb2.FunctionUpdateSchedulingParamsRequest(function_id=self.object_id, settings=settings)
1331
- await retry_transient_errors(self.client.stub.FunctionUpdateSchedulingParams, request)
1344
+ await self.client.stub.FunctionUpdateSchedulingParams(request)
1332
1345
 
1333
1346
  # One idea would be for FunctionUpdateScheduleParams to return the current (coalesced) settings
1334
1347
  # and then we could return them here (would need some ad hoc dataclass, which I don't love)
@@ -1375,34 +1388,43 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1375
1388
  cls,
1376
1389
  app_name: str,
1377
1390
  name: str,
1378
- namespace=None, # mdmd:line-hidden
1379
- environment_name: Optional[str] = None,
1391
+ *,
1392
+ load_context_overrides: LoadContext,
1380
1393
  ):
1381
1394
  # internal function lookup implementation that allows lookup of class "service functions"
1382
1395
  # in addition to non-class functions
1383
- async def _load_remote(self: _Function, resolver: Resolver, existing_object_id: Optional[str]):
1384
- assert resolver.client and resolver.client.stub
1396
+ async def _load_remote(
1397
+ self: _Function, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
1398
+ ):
1385
1399
  request = api_pb2.FunctionGetRequest(
1386
1400
  app_name=app_name,
1387
1401
  object_tag=name,
1388
- environment_name=_get_environment_name(environment_name, resolver) or "",
1402
+ environment_name=load_context.environment_name,
1389
1403
  )
1390
1404
  try:
1391
- response = await retry_transient_errors(resolver.client.stub.FunctionGet, request)
1405
+ response = await load_context.client.stub.FunctionGet(request)
1392
1406
  except NotFoundError as exc:
1393
1407
  # refine the error message
1394
- env_context = f" (in the '{environment_name}' environment)" if environment_name else ""
1408
+ env_context = (
1409
+ f" (in the '{load_context.environment_name}' environment)" if load_context.environment_name else ""
1410
+ )
1395
1411
  raise NotFoundError(
1396
1412
  f"Lookup failed for Function '{name}' from the '{app_name}' app{env_context}: {exc}."
1397
1413
  ) from None
1398
1414
 
1399
1415
  print_server_warnings(response.server_warnings)
1400
1416
 
1401
- self._hydrate(response.function_id, resolver.client, response.handle_metadata)
1417
+ self._hydrate(response.function_id, load_context.client, response.handle_metadata)
1402
1418
 
1403
- environment_rep = f", environment_name={environment_name!r}" if environment_name else ""
1419
+ environment_rep = (
1420
+ f", environment_name={load_context_overrides.environment_name!r}"
1421
+ if load_context_overrides._environment_name # slightly ugly - checking if _environment_name is overridden
1422
+ else ""
1423
+ )
1404
1424
  rep = f"modal.Function.from_name('{app_name}', '{name}'{environment_rep})"
1405
- return cls._from_loader(_load_remote, rep, is_another_app=True, hydrate_lazily=True)
1425
+ return cls._from_loader(
1426
+ _load_remote, rep, is_another_app=True, hydrate_lazily=True, load_context_overrides=load_context_overrides
1427
+ )
1406
1428
 
1407
1429
  @classmethod
1408
1430
  def from_name(
@@ -1412,6 +1434,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1412
1434
  *,
1413
1435
  namespace=None, # mdmd:line-hidden
1414
1436
  environment_name: Optional[str] = None,
1437
+ client: Optional[_Client] = None,
1415
1438
  ) -> "_Function":
1416
1439
  """Reference a Function from a deployed App by its name.
1417
1440
 
@@ -1435,7 +1458,9 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1435
1458
  )
1436
1459
 
1437
1460
  warn_if_passing_namespace(namespace, "modal.Function.from_name")
1438
- return cls._from_name(app_name, name, environment_name=environment_name)
1461
+ return cls._from_name(
1462
+ app_name, name, load_context_overrides=LoadContext(environment_name=environment_name, client=client)
1463
+ )
1439
1464
 
1440
1465
  @property
1441
1466
  def tag(self) -> str:
@@ -1658,8 +1683,8 @@ Use the `Function.get_web_url()` method instead.
1658
1683
  input_queue,
1659
1684
  self.client,
1660
1685
  )
1661
- metadata = api_pb2.FunctionCallFromIdResponse(function_call_id=function_call_id, num_inputs=num_inputs)
1662
- fc: _FunctionCall[ReturnType] = _FunctionCall._new_hydrated(function_call_id, self.client, metadata)
1686
+ fc: _FunctionCall[ReturnType] = _FunctionCall._new_hydrated(function_call_id, self.client, None)
1687
+ fc._num_inputs = num_inputs # set the cached value of num_inputs
1663
1688
  return fc
1664
1689
 
1665
1690
  async def _call_function(self, args, kwargs) -> ReturnType:
@@ -1888,10 +1913,9 @@ Use the `Function.get_web_url()` method instead.
1888
1913
  @live_method
1889
1914
  async def get_current_stats(self) -> FunctionStats:
1890
1915
  """Return a `FunctionStats` object describing the current function's queue and runner counts."""
1891
- resp = await retry_transient_errors(
1892
- self.client.stub.FunctionGetCurrentStats,
1916
+ resp = await self.client.stub.FunctionGetCurrentStats(
1893
1917
  api_pb2.FunctionGetCurrentStatsRequest(function_id=self.object_id),
1894
- total_timeout=10.0,
1918
+ retry=Retry(total_timeout=10.0),
1895
1919
  )
1896
1920
  return FunctionStats(backlog=resp.backlog, num_total_runners=resp.num_total_tasks)
1897
1921
 
@@ -1929,19 +1953,16 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
1929
1953
  def _invocation(self):
1930
1954
  return _Invocation(self.client.stub, self.object_id, self.client)
1931
1955
 
1932
- def _hydrate_metadata(self, metadata: Optional[Message]):
1933
- if not metadata:
1934
- return
1935
- assert isinstance(metadata, api_pb2.FunctionCallFromIdResponse)
1936
- self._num_inputs = metadata.num_inputs
1937
-
1938
1956
  @live_method
1939
1957
  async def num_inputs(self) -> int:
1940
1958
  """Get the number of inputs in the function call."""
1941
- # Should have been hydrated.
1942
- assert self._num_inputs is not None
1959
+ if self._num_inputs is None:
1960
+ request = api_pb2.FunctionCallFromIdRequest(function_call_id=self.object_id)
1961
+ resp = await self.client.stub.FunctionCallFromId(request)
1962
+ self._num_inputs = resp.num_inputs # cached
1943
1963
  return self._num_inputs
1944
1964
 
1965
+ @live_method
1945
1966
  async def get(self, timeout: Optional[float] = None, *, index: int = 0) -> ReturnType:
1946
1967
  """Get the result of the index-th input of the function call.
1947
1968
  `.spawn()` calls have a single output, so only specifying `index=0` is valid.
@@ -1985,6 +2006,7 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
1985
2006
  async for _, item in self._invocation().enumerate(start_index=start, end_index=end):
1986
2007
  yield item
1987
2008
 
2009
+ @live_method
1988
2010
  async def get_call_graph(self) -> list[InputInfo]:
1989
2011
  """Returns a structure representing the call graph from a given root
1990
2012
  call ID, along with the status of execution for each node.
@@ -1994,9 +2016,10 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
1994
2016
  """
1995
2017
  assert self._client and self._client.stub
1996
2018
  request = api_pb2.FunctionGetCallGraphRequest(function_call_id=self.object_id)
1997
- response = await retry_transient_errors(self._client.stub.FunctionGetCallGraph, request)
2019
+ response = await self._client.stub.FunctionGetCallGraph(request)
1998
2020
  return _reconstruct_call_graph(response)
1999
2021
 
2022
+ @live_method
2000
2023
  async def cancel(
2001
2024
  self,
2002
2025
  # if true, containers running the inputs are forcibly terminated
@@ -2012,10 +2035,13 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
2012
2035
  function_call_id=self.object_id, terminate_containers=terminate_containers
2013
2036
  )
2014
2037
  assert self._client and self._client.stub
2015
- await retry_transient_errors(self._client.stub.FunctionCallCancel, request)
2038
+ await self._client.stub.FunctionCallCancel(request)
2016
2039
 
2017
- @staticmethod
2018
- async def from_id(function_call_id: str, client: Optional[_Client] = None) -> "_FunctionCall[Any]":
2040
+ @deprecate_aio_usage((2025, 11, 14), "FunctionCall.from_id")
2041
+ @classmethod
2042
+ def from_id(
2043
+ cls, function_call_id: str, client: Optional["modal.client.Client"] = None
2044
+ ) -> "modal.functions.FunctionCall[Any]":
2019
2045
  """Instantiate a FunctionCall object from an existing ID.
2020
2046
 
2021
2047
  Examples:
@@ -2026,7 +2052,7 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
2026
2052
  fc_id = fc.object_id
2027
2053
 
2028
2054
  # Later, use the ID to re-instantiate the FunctionCall object
2029
- fc = _FunctionCall.from_id(fc_id)
2055
+ fc = FunctionCall.from_id(fc_id)
2030
2056
  result = fc.get()
2031
2057
  ```
2032
2058
 
@@ -2034,20 +2060,19 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
2034
2060
  if you no longer have access to the original object returned from `Function.spawn`.
2035
2061
 
2036
2062
  """
2037
- if client is None:
2038
- client = await _Client.from_env()
2063
+ _client = typing.cast(_Client, synchronizer._translate_in(client))
2039
2064
 
2040
- async def _load(self: _FunctionCall, resolver: Resolver, existing_object_id: Optional[str]):
2041
- request = api_pb2.FunctionCallFromIdRequest(function_call_id=function_call_id)
2042
- resp = await retry_transient_errors(resolver.client.stub.FunctionCallFromId, request)
2043
- self._hydrate(function_call_id, resolver.client, resp)
2065
+ async def _load(
2066
+ self: _FunctionCall, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
2067
+ ):
2068
+ # this loader doesn't do anything in practice, but it will get the client from the load_context
2069
+ self._hydrate(function_call_id, load_context.client, None)
2044
2070
 
2045
2071
  rep = f"FunctionCall.from_id({function_call_id!r})"
2046
- fc: _FunctionCall[Any] = _FunctionCall._from_loader(_load, rep, hydrate_lazily=True)
2047
- # We already know the object ID, so we can set it directly
2048
- fc._object_id = function_call_id
2049
- fc._client = client
2050
- return fc
2072
+ impl_instance = _FunctionCall._from_loader(
2073
+ _load, rep, hydrate_lazily=True, load_context_overrides=LoadContext(client=_client)
2074
+ )
2075
+ return typing.cast("modal.functions.FunctionCall[Any]", synchronizer._translate_out(impl_instance))
2051
2076
 
2052
2077
  @staticmethod
2053
2078
  async def gather(*function_calls: "_FunctionCall[T]") -> typing.Sequence[T]: