feat(sandbox): e2b code execution primitive (#30898)

* feat(sandbox): add e2b code execution primitive

Add a provider-agnostic code execution primitive that runs model-generated
code in an isolated sandbox and returns the output, with e2b as the first
backend over raw httpx (no SDK dependency).

Public API: litellm.acode_interpreter_tool (ephemeral create -> run -> delete)
plus the low-level lifecycle litellm.acreate_sandbox / arun_code /
adelete_sandbox. Each is @client-decorated so operations are logged like
litellm.asearch. Backends implement BaseSandboxConfig; resolved via
ProviderConfigManager.get_provider_sandbox_config.

* fix(sandbox): address review feedback and CI gates

- document e2b provider in provider_endpoints_support.json and add a sandbox endpoint definition
- regenerate dashboard CallTypes after the sandbox call-type additions
- guard explicit timeout=0 instead of coercing it to the default
- require a ContainerHandle access token before running code; reject bare-id runs
- return False on a 404 delete now that the shared http handler raises for status
- skip non-JSON NDJSON lines and cap streamed output to bound memory
- move the real-network integration tests out of tests/test_litellm into tests/integration/sandbox

* fix(sandbox): satisfy strict ruff gate and scope star-exports

- modernize annotations in the new sandbox modules to PEP 585/604 (list/dict,
  X | None) and drop the now-unnecessary quoted forward refs so the strict-rule
  budget delta for UP006/UP037/UP045 returns to zero
- add __all__ to litellm/sandbox/main.py so 'import *' only re-exports the four
  public entrypoints instead of leaking module-level imports

* fix(sandbox): drop quotes on sandbox config return annotation

utils.py uses 'from __future__ import annotations', so the quoted forward ref
tripped UP037; the unquoted union is lazily evaluated and keeps the strict-rule
delta at zero

* chore(sandbox): re-trigger automated review after addressing feedback
This commit is contained in:
Krrish Dholakia 2026-06-20 16:30:01 -07:00 committed by GitHub
parent b16cfd7de9
commit 53593f697d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 838 additions and 1 deletions

View file

@ -1401,6 +1401,7 @@ from .skills.main import (
from .containers.main import *
from .ocr.main import *
from .rag.main import *
from .sandbox.main import *
from .search.main import *
from .realtime_api.main import (
_arealtime,

View file

@ -0,0 +1,79 @@
"""
Base Sandbox transformation configuration.
A sandbox provider runs an executable string inside an isolated container and
returns whatever the sandbox produced. The lifecycle is create container ->
run code -> delete container; `code_interpreter_tool` combines all three.
"""
from typing import Any, Union
from pydantic import Field, PrivateAttr
from litellm.types.llms.base import LiteLLMPydanticObjectBase
class ContainerHandle(LiteLLMPydanticObjectBase):
"""A live sandbox container. Carries everything needed to reach it again."""
id: str
provider: str
domain: str | None = None
model_config = {"extra": "allow"}
_hidden_params: dict = PrivateAttr(default_factory=dict)
class CodeExecutionResult(LiteLLMPydanticObjectBase):
"""Passthrough of the sandbox's own execution output."""
stdout: str = ""
stderr: str = ""
results: list[dict[str, Any]] = Field(default_factory=list)
error: dict[str, Any] | None = None
execution_count: int | None = None
object: str = "code_execution"
model_config = {"extra": "allow"}
_hidden_params: dict = PrivateAttr(default_factory=dict)
class BaseSandboxConfig:
"""Provider-agnostic sandbox operations."""
def validate_environment(self, api_key: str | None = None, **kwargs) -> str:
raise NotImplementedError(
"validate_environment must be implemented by provider"
)
async def acreate_sandbox(
self,
*,
template: str | None = None,
timeout: int | None = None,
allow_internet_access: bool = True,
api_key: str | None = None,
**kwargs,
) -> ContainerHandle:
raise NotImplementedError("acreate_sandbox must be implemented by provider")
async def arun_code(
self,
*,
container: Union[ContainerHandle, str],
code: str,
api_key: str | None = None,
**kwargs,
) -> CodeExecutionResult:
raise NotImplementedError("arun_code must be implemented by provider")
async def adelete_sandbox(
self,
*,
container: Union[ContainerHandle, str],
api_key: str | None = None,
**kwargs,
) -> bool:
raise NotImplementedError("adelete_sandbox must be implemented by provider")

View file

View file

View file

@ -0,0 +1,219 @@
"""
e2b sandbox provider.
Talks to e2b's REST API directly over httpx (no e2b SDK dependency):
- create: POST {api_base}/sandboxes
- run: POST https://{JUPYTER_PORT}-{sandboxID}.{domain}/execute (NDJSON stream)
- delete: DELETE {api_base}/sandboxes/{sandboxID}
"""
import json
from typing import Union, cast
import httpx
from litellm.llms.base_llm.sandbox.transformation import (
BaseSandboxConfig,
CodeExecutionResult,
ContainerHandle,
)
from litellm.llms.custom_httpx.http_handler import (
AsyncHTTPHandler,
get_async_httpx_client,
)
from litellm.secret_managers.main import get_secret_str
from litellm.types.llms.custom_http import httpxSpecialProvider
E2B_API_BASE = "https://api.e2b.app"
E2B_DEFAULT_TEMPLATE = "code-interpreter-v1"
E2B_DEFAULT_DOMAIN = "e2b.app"
JUPYTER_PORT = 49999
DEFAULT_SANDBOX_TIMEOUT = 300
MAX_OUTPUT_BYTES = 10 * 1024 * 1024
class E2BSandboxConfig(BaseSandboxConfig):
def _http(self, client: AsyncHTTPHandler | None) -> AsyncHTTPHandler:
if client is not None:
return client
return get_async_httpx_client(llm_provider=httpxSpecialProvider.Sandbox)
def validate_environment(self, api_key: str | None = None, **kwargs) -> str:
key = api_key or get_secret_str("E2B_API_KEY")
if not key:
raise ValueError("E2B API key not set. Set E2B_API_KEY or pass api_key=...")
return key
async def acreate_sandbox(
self,
*,
template: str | None = None,
timeout: int | None = None,
allow_internet_access: bool = True,
api_key: str | None = None,
metadata: dict | None = None,
client: AsyncHTTPHandler | None = None,
**kwargs,
) -> ContainerHandle:
key = self.validate_environment(api_key=api_key)
body = {
"templateID": template or E2B_DEFAULT_TEMPLATE,
"timeout": timeout if timeout is not None else DEFAULT_SANDBOX_TIMEOUT,
"secure": True,
"allow_internet_access": allow_internet_access,
}
if metadata:
body["metadata"] = metadata
response = cast(
httpx.Response,
await self._http(client).post(
url=f"{E2B_API_BASE}/sandboxes",
headers={"X-API-Key": key, "Content-Type": "application/json"},
json=body,
),
)
data = response.json()
handle = ContainerHandle(
id=data["sandboxID"],
provider="e2b",
domain=data.get("domain") or E2B_DEFAULT_DOMAIN,
)
handle._hidden_params = {
"envd_access_token": data.get("envdAccessToken"),
"traffic_access_token": data.get("trafficAccessToken"),
"api_key": key,
}
return handle
async def arun_code(
self,
*,
container: Union[ContainerHandle, str],
code: str,
api_key: str | None = None,
env_vars: dict | None = None,
client: AsyncHTTPHandler | None = None,
**kwargs,
) -> CodeExecutionResult:
handle = self._as_handle(container)
token = handle._hidden_params.get("envd_access_token")
if not token:
raise ValueError(
"Cannot run code from a sandbox id alone. e2b secure sandboxes "
"require the access token returned by acreate_sandbox; pass the "
"ContainerHandle it returned instead of a bare sandbox id."
)
headers = {"Content-Type": "application/json", "X-Access-Token": token}
traffic_token = handle._hidden_params.get("traffic_access_token")
if traffic_token:
headers["E2B-Traffic-Access-Token"] = traffic_token
url = f"https://{JUPYTER_PORT}-{handle.id}.{handle.domain}/execute"
response = cast(
httpx.Response,
await self._http(client).post(
url=url,
headers=headers,
json={"code": code, "context_id": None, "env_vars": env_vars},
stream=True,
),
)
lines = await self._read_capped_lines(response)
return self._parse_lines(lines)
async def adelete_sandbox(
self,
*,
container: Union[ContainerHandle, str],
api_key: str | None = None,
client: AsyncHTTPHandler | None = None,
**kwargs,
) -> bool:
handle = self._as_handle(container)
key = (
api_key
or handle._hidden_params.get("api_key")
or self.validate_environment()
)
try:
response = cast(
httpx.Response,
await self._http(client).delete(
url=f"{E2B_API_BASE}/sandboxes/{handle.id}",
headers={"X-API-Key": key},
),
)
except httpx.HTTPStatusError as e:
if e.response.status_code == 404:
return False
raise
return 200 <= response.status_code < 300
@staticmethod
def _as_handle(container: Union[ContainerHandle, str]) -> ContainerHandle:
if isinstance(container, ContainerHandle):
return container
handle = ContainerHandle(
id=str(container), provider="e2b", domain=E2B_DEFAULT_DOMAIN
)
handle._hidden_params = {}
return handle
@staticmethod
async def _read_capped_lines(response: httpx.Response) -> list[str]:
lines: list[str] = []
total = 0
async for line in response.aiter_lines():
total += len(line.encode("utf-8"))
if total > MAX_OUTPUT_BYTES:
raise ValueError(
f"Sandbox output exceeded {MAX_OUTPUT_BYTES} bytes; aborting to "
"avoid unbounded memory use."
)
lines.append(line)
return lines
@staticmethod
def _parse_lines(lines: list[str]) -> CodeExecutionResult:
def _try_parse(stripped: str):
try:
return json.loads(stripped)
except json.JSONDecodeError:
return None
messages = tuple(
parsed
for stripped in (line.strip() for line in lines)
if stripped
for parsed in (_try_parse(stripped),)
if parsed is not None
)
def of_type(message_type: str):
return (m for m in messages if m.get("type") == message_type)
error = next(
(
{key: m.get(key) for key in ("name", "value", "traceback")}
for m in of_type("error")
),
None,
)
execution_count = next(
(m.get("execution_count") for m in of_type("number_of_executions")),
None,
)
return CodeExecutionResult(
stdout="".join(m.get("text", "") for m in of_type("stdout")),
stderr="".join(m.get("text", "") for m in of_type("stderr")),
results=[
{k: v for k, v in m.items() if k != "type"} for m in of_type("result")
],
error=error,
execution_count=execution_count,
)

View file

145
litellm/sandbox/main.py Normal file
View file

@ -0,0 +1,145 @@
"""
Public entrypoints for running model-generated code in a sandbox.
Low-level lifecycle:
acreate_container -> arun_code -> adelete_container
High-level convenience:
acode_interpreter_tool (ephemeral: create -> run -> delete)
Each entrypoint is `@client`-decorated, so every operation is logged the same
way `litellm.asearch` is.
"""
from typing import Union
import litellm
from litellm.llms.base_llm.sandbox.transformation import (
BaseSandboxConfig,
CodeExecutionResult,
ContainerHandle,
)
from litellm.types.utils import SandboxProviders
from litellm.utils import ProviderConfigManager, client
__all__ = [
"acreate_sandbox",
"arun_code",
"adelete_sandbox",
"acode_interpreter_tool",
]
_LITELLM_INTERNAL_KWARGS = {
"litellm_logging_obj",
"litellm_call_id",
"litellm_trace_id",
"litellm_metadata",
}
def _get_config(provider: str) -> BaseSandboxConfig:
config = ProviderConfigManager.get_provider_sandbox_config(
SandboxProviders(provider)
)
if config is None:
raise ValueError(f"Code execution is not supported for provider: {provider}")
return config
def _forward_kwargs(kwargs: dict) -> dict:
return {k: v for k, v in kwargs.items() if k not in _LITELLM_INTERNAL_KWARGS}
def _update_logging(kwargs: dict, provider: str, operation: str) -> None:
logging_obj = kwargs.get("litellm_logging_obj")
if logging_obj is None:
return
logging_obj.update_from_kwargs(
kwargs=kwargs,
model=f"{provider}/{operation}",
optional_params={},
litellm_params={"litellm_call_id": kwargs.get("litellm_call_id")},
custom_llm_provider=provider,
)
@client
async def acreate_sandbox(
provider: str,
template: str | None = None,
timeout: int | None = None,
allow_internet_access: bool = True,
api_key: str | None = None,
**kwargs,
) -> ContainerHandle:
_update_logging(kwargs, provider, "create_sandbox")
return await _get_config(provider).acreate_sandbox(
template=template,
timeout=timeout,
allow_internet_access=allow_internet_access,
api_key=api_key,
**_forward_kwargs(kwargs),
)
@client
async def arun_code(
provider: str,
container: Union[ContainerHandle, str],
code: str,
api_key: str | None = None,
**kwargs,
) -> CodeExecutionResult:
_update_logging(kwargs, provider, "run_code")
return await _get_config(provider).arun_code(
container=container,
code=code,
api_key=api_key,
**_forward_kwargs(kwargs),
)
@client
async def adelete_sandbox(
provider: str,
container: Union[ContainerHandle, str],
api_key: str | None = None,
**kwargs,
) -> bool:
_update_logging(kwargs, provider, "delete_sandbox")
return await _get_config(provider).adelete_sandbox(
container=container,
api_key=api_key,
**_forward_kwargs(kwargs),
)
@client
async def acode_interpreter_tool(
provider: str,
code: str,
template: str | None = None,
timeout: int | None = None,
api_key: str | None = None,
**kwargs,
) -> CodeExecutionResult:
_update_logging(kwargs, provider, "code_interpreter_tool")
config = _get_config(provider)
forwarded = _forward_kwargs(kwargs)
container = await config.acreate_sandbox(
template=template, timeout=timeout, api_key=api_key, **forwarded
)
try:
return await config.arun_code(
container=container, code=code, api_key=api_key, **forwarded
)
finally:
try:
await config.adelete_sandbox(
container=container, api_key=api_key, **forwarded
)
except Exception as e:
litellm._logging.verbose_logger.debug(
f"sandbox: failed to delete ephemeral container: {e}"
)

View file

@ -29,6 +29,7 @@ class httpxSpecialProvider(str, Enum):
A2A = "a2a"
PromptManagement = "prompt_management"
UI = "ui"
Sandbox = "sandbox"
VerifyTypes = Union[str, bool, ssl.SSLContext]

View file

@ -435,6 +435,14 @@ class CallTypes(str, Enum):
alist_container_files = "alist_container_files"
upload_container_file = "upload_container_file"
aupload_container_file = "aupload_container_file"
create_sandbox = "create_sandbox"
acreate_sandbox = "acreate_sandbox"
delete_sandbox = "delete_sandbox"
adelete_sandbox = "adelete_sandbox"
run_code = "run_code"
arun_code = "arun_code"
code_interpreter_tool = "code_interpreter_tool"
acode_interpreter_tool = "acode_interpreter_tool"
acancel_fine_tuning_job = "acancel_fine_tuning_job"
cancel_fine_tuning_job = "cancel_fine_tuning_job"
@ -3490,6 +3498,15 @@ class SearchProviders(str, Enum):
SearchProvidersSet = {provider.value for provider in SearchProviders}
class SandboxProviders(str, Enum):
"""
Enum for code execution sandbox provider types.
Separate from LlmProviders for semantic clarity.
"""
E2B = "e2b"
class LiteLLMLoggingBaseClass:
"""
Base class for logging pre and post call

View file

@ -192,6 +192,7 @@ from litellm.types.utils import (
ProviderField,
ProviderSpecificModelInfo,
RawRequestTypedDict,
SandboxProviders,
SearchProviders,
SelectTokenizerResponse,
StreamingChoices,
@ -301,6 +302,7 @@ if TYPE_CHECKING:
BaseGoogleGenAIGenerateContentConfig,
)
from litellm.llms.base_llm.ocr.transformation import BaseOCRConfig
from litellm.llms.base_llm.sandbox.transformation import BaseSandboxConfig
from litellm.llms.base_llm.search.transformation import BaseSearchConfig
from litellm.llms.base_llm.text_to_speech.transformation import (
BaseTextToSpeechConfig,
@ -9723,6 +9725,19 @@ class ProviderConfigManager:
return None
return config_class()
@staticmethod
def get_provider_sandbox_config(
provider: SandboxProviders,
) -> BaseSandboxConfig | None:
"""
Get sandbox (code execution) configuration for a given provider.
"""
from litellm.llms.e2b.sandbox.transformation import E2BSandboxConfig
if provider == SandboxProviders.E2B:
return E2BSandboxConfig()
return None
@staticmethod
def get_provider_text_to_speech_config(
model: str,

View file

@ -831,6 +831,23 @@
"search": true
}
},
"e2b": {
"display_name": "E2B (`e2b`)",
"url": "https://docs.litellm.ai/docs/providers/e2b",
"endpoints": {
"chat_completions": false,
"messages": false,
"responses": false,
"embeddings": false,
"image_generations": false,
"audio_transcriptions": false,
"audio_speech": false,
"moderations": false,
"batches": false,
"rerank": false,
"sandbox": true
}
},
"elevenlabs": {
"display_name": "ElevenLabs (`elevenlabs`)",
"url": "https://docs.litellm.ai/docs/providers/elevenlabs",
@ -3056,6 +3073,13 @@
"provider_json_field": "compact",
"url": "https://docs.litellm.ai/docs/response_api"
},
"sandbox": {
"docs_label": "sandbox",
"display_name": "Code Execution Sandbox API",
"leftnav_label": "/sandbox",
"provider_json_field": "sandbox",
"url": "https://docs.litellm.ai/docs/providers/e2b"
},
"search": {
"docs_label": "search",
"display_name": "Search API",

View file

@ -0,0 +1,39 @@
"""
e2b code execution sandbox - end-to-end integration tests.
These tests make REAL HTTP calls to the e2b API and are skipped automatically
unless E2B_API_KEY is set. Mock-only unit tests live in
tests/test_litellm/sandbox/test_e2b_sandbox.py.
Run only these tests:
pytest tests/integration/sandbox/test_e2b_sandbox.py -v
"""
import os
import pytest
import litellm
@pytest.mark.skipif("E2B_API_KEY" not in os.environ, reason="needs a real E2B_API_KEY")
@pytest.mark.asyncio
async def test_integration_ephemeral_real_e2b():
result = await litellm.acode_interpreter_tool(
provider="e2b", code="print(sum(range(10)))"
)
assert result.stdout.strip() == "45"
assert result.error is None
@pytest.mark.skipif("E2B_API_KEY" not in os.environ, reason="needs a real E2B_API_KEY")
@pytest.mark.asyncio
async def test_integration_lifecycle_roundtrip_real_e2b():
container = await litellm.acreate_sandbox(provider="e2b")
try:
result = await litellm.arun_code(
provider="e2b", container=container, code="print(6*7)"
)
assert result.stdout.strip() == "42"
finally:
assert await litellm.adelete_sandbox(provider="e2b", container=container)

View file

@ -0,0 +1,297 @@
"""
Tests for the e2b code execution sandbox primitive.
Unit tests inject a fake async HTTP client (dependency injection, no
monkeypatching) and assert request shapes and result mapping. Real-network
integration tests live in tests/integration/sandbox/test_e2b_sandbox.py.
"""
import json
import httpx
import pytest
import litellm
from litellm.llms.base_llm.sandbox.transformation import ContainerHandle
from litellm.llms.e2b.sandbox.transformation import (
MAX_OUTPUT_BYTES,
E2BSandboxConfig,
)
class FakeResponse:
def __init__(self, *, json_data=None, lines=None, status_code=200):
self._json = json_data
self._lines = lines or []
self.status_code = status_code
def json(self):
return self._json
async def aiter_lines(self):
for line in self._lines:
yield line
class FakeHTTPClient:
"""Records outbound requests and returns canned responses keyed by URL."""
def __init__(
self,
*,
create_json=None,
execute_lines=None,
delete_status=204,
execute_raises=None,
):
self.create_json = create_json or {
"sandboxID": "sbx_123",
"domain": "e2b.app",
"envdAccessToken": "tok_abc",
}
self.execute_lines = execute_lines or []
self.delete_status = delete_status
self.execute_raises = execute_raises
self.calls = []
async def post(self, url, headers=None, json=None, stream=False, **kwargs):
self.calls.append(("POST", url, headers, json))
if url.endswith("/sandboxes"):
return FakeResponse(json_data=self.create_json)
if url.endswith("/execute"):
if self.execute_raises is not None:
raise self.execute_raises
return FakeResponse(lines=self.execute_lines)
raise AssertionError(f"unexpected POST {url}")
async def delete(self, url, headers=None, **kwargs):
self.calls.append(("DELETE", url, headers, None))
if not (200 <= self.delete_status < 300):
raise httpx.HTTPStatusError(
f"status {self.delete_status}",
request=httpx.Request("DELETE", url),
response=httpx.Response(self.delete_status),
)
return FakeResponse(status_code=self.delete_status)
# ---------- pure parser ----------
def test_parse_lines_stdout_and_count():
lines = [
json.dumps({"type": "stdout", "text": "6\n", "timestamp": 1}),
json.dumps({"type": "number_of_executions", "execution_count": 1}),
]
result = E2BSandboxConfig._parse_lines(lines)
assert result.stdout == "6\n"
assert result.execution_count == 1
assert result.error is None
def test_parse_lines_error_surfaces_name_and_traceback():
lines = [
json.dumps(
{
"type": "error",
"name": "ZeroDivisionError",
"value": "division by zero",
"traceback": "Traceback (most recent call last): ...",
}
)
]
result = E2BSandboxConfig._parse_lines(lines)
assert result.error["name"] == "ZeroDivisionError"
assert "Traceback" in result.error["traceback"]
def test_parse_lines_result_carries_png():
lines = [
json.dumps({"type": "result", "png": "BASE64DATA", "is_main_result": True})
]
result = E2BSandboxConfig._parse_lines(lines)
assert result.results and result.results[0]["png"] == "BASE64DATA"
assert "type" not in result.results[0]
# ---------- request shapes ----------
@pytest.mark.asyncio
async def test_template_flows_into_create_request_as_templateID():
client = FakeHTTPClient()
cfg = E2BSandboxConfig()
handle = await cfg.acreate_sandbox(
template="my-custom-template", api_key="e2b_key", client=client
)
method, url, headers, body = client.calls[0]
assert method == "POST"
assert url.endswith("/sandboxes")
assert body["templateID"] == "my-custom-template" # not "template"
assert body["secure"] is True
assert headers["X-API-Key"] == "e2b_key"
assert handle.id == "sbx_123"
assert handle._hidden_params["envd_access_token"] == "tok_abc"
@pytest.mark.asyncio
async def test_create_defaults_template_when_omitted():
client = FakeHTTPClient()
await E2BSandboxConfig().acreate_sandbox(api_key="e2b_key", client=client)
_, _, _, body = client.calls[0]
assert body["templateID"] == "code-interpreter-v1"
@pytest.mark.asyncio
async def test_run_code_targets_jupyter_host_with_access_token():
client = FakeHTTPClient(
execute_lines=[json.dumps({"type": "stdout", "text": "42\n", "timestamp": 1})]
)
handle = ContainerHandle(id="sbx_xyz", provider="e2b", domain="e2b.app")
handle._hidden_params = {"envd_access_token": "tok_run"}
result = await E2BSandboxConfig().arun_code(
container=handle, code="print(6*7)", client=client
)
method, url, headers, body = client.calls[0]
assert url == "https://49999-sbx_xyz.e2b.app/execute"
assert headers["X-Access-Token"] == "tok_run"
assert body["code"] == "print(6*7)"
assert result.stdout.strip() == "42"
@pytest.mark.asyncio
async def test_delete_issues_delete_to_sandbox_id():
client = FakeHTTPClient(delete_status=204)
handle = ContainerHandle(id="sbx_del", provider="e2b", domain="e2b.app")
handle._hidden_params = {"api_key": "e2b_key"}
ok = await E2BSandboxConfig().adelete_sandbox(container=handle, client=client)
method, url, headers, _ = client.calls[0]
assert method == "DELETE"
assert url.endswith("/sandboxes/sbx_del")
assert ok is True
@pytest.mark.asyncio
async def test_delete_returns_false_on_404():
client = FakeHTTPClient(delete_status=404)
handle = ContainerHandle(id="sbx_gone", provider="e2b", domain="e2b.app")
handle._hidden_params = {"api_key": "e2b_key"}
ok = await E2BSandboxConfig().adelete_sandbox(container=handle, client=client)
assert ok is False
# ---------- ephemeral teardown ----------
@pytest.mark.asyncio
async def test_code_interpreter_tool_deletes_even_when_run_raises():
client = FakeHTTPClient(execute_raises=RuntimeError("boom"))
with pytest.raises(RuntimeError, match="boom"):
await litellm.acode_interpreter_tool(
provider="e2b", code="1/0", api_key="e2b_key", client=client
)
methods = [c[0] for c in client.calls]
urls = [c[1] for c in client.calls]
assert methods == ["POST", "POST", "DELETE"] # create, run(raises), delete
assert urls[0].endswith("/sandboxes")
assert urls[1].endswith("/execute")
assert urls[2].endswith("/sandboxes/sbx_123")
# ---------- correctness guards ----------
@pytest.mark.asyncio
async def test_delete_reraises_non_404_http_error():
client = FakeHTTPClient(delete_status=500)
handle = ContainerHandle(id="sbx_err", provider="e2b", domain="e2b.app")
handle._hidden_params = {"api_key": "e2b_key"}
with pytest.raises(httpx.HTTPStatusError):
await E2BSandboxConfig().adelete_sandbox(container=handle, client=client)
@pytest.mark.asyncio
async def test_create_preserves_explicit_zero_timeout():
client = FakeHTTPClient()
await E2BSandboxConfig().acreate_sandbox(
timeout=0, api_key="e2b_key", client=client
)
_, _, _, body = client.calls[0]
assert body["timeout"] == 0
@pytest.mark.asyncio
async def test_run_code_rejects_bare_id_without_access_token():
client = FakeHTTPClient()
with pytest.raises(ValueError, match="access token"):
await E2BSandboxConfig().arun_code(
container="sbx_no_token", code="print(1)", client=client
)
assert client.calls == [] # never reached the network
def test_parse_lines_skips_non_json_lines():
lines = [
"not-json-heartbeat",
json.dumps({"type": "stdout", "text": "ok\n"}),
"",
"{partial",
]
result = E2BSandboxConfig._parse_lines(lines)
assert result.stdout == "ok\n"
assert result.error is None
@pytest.mark.asyncio
async def test_run_code_aborts_on_output_over_cap():
big_line = "x" * (MAX_OUTPUT_BYTES + 1)
client = FakeHTTPClient(execute_lines=[big_line])
handle = ContainerHandle(id="sbx_big", provider="e2b", domain="e2b.app")
handle._hidden_params = {"envd_access_token": "tok"}
with pytest.raises(ValueError, match="exceeded"):
await E2BSandboxConfig().arun_code(
container=handle, code="print('x'*999)", client=client
)
# ---------- public entrypoints ----------
@pytest.mark.asyncio
async def test_public_lifecycle_create_run_delete():
client = FakeHTTPClient(
execute_lines=[json.dumps({"type": "stdout", "text": "42\n"})]
)
container = await litellm.acreate_sandbox(
provider="e2b", api_key="e2b_key", client=client
)
assert container.id == "sbx_123"
result = await litellm.arun_code(
provider="e2b",
container=container,
api_key="e2b_key",
code="print(6*7)",
client=client,
)
assert result.stdout.strip() == "42"
assert (
await litellm.adelete_sandbox(
provider="e2b", container=container, api_key="e2b_key", client=client
)
is True
)
@pytest.mark.asyncio
async def test_unsupported_provider_raises():
with pytest.raises(ValueError):
await litellm.acreate_sandbox(provider="not-a-provider")

View file

@ -21352,7 +21352,7 @@ export interface components {
* CallTypes
* @enum {string}
*/
CallTypes: "embedding" | "aembedding" | "completion" | "acompletion" | "atext_completion" | "text_completion" | "image_generation" | "aimage_generation" | "image_edit" | "aimage_edit" | "moderation" | "amoderation" | "atranscription" | "transcription" | "aspeech" | "speech" | "rerank" | "arerank" | "search" | "asearch" | "_arealtime" | "_aresponses_websocket" | "create_batch" | "acreate_batch" | "aretrieve_batch" | "retrieve_batch" | "acancel_batch" | "cancel_batch" | "pass_through_endpoint" | "anthropic_messages" | "get_assistants" | "aget_assistants" | "create_assistants" | "acreate_assistants" | "delete_assistant" | "adelete_assistant" | "acreate_thread" | "create_thread" | "aget_thread" | "get_thread" | "a_add_message" | "add_message" | "aget_messages" | "get_messages" | "arun_thread" | "run_thread" | "arun_thread_stream" | "run_thread_stream" | "afile_retrieve" | "file_retrieve" | "afile_delete" | "file_delete" | "afile_list" | "file_list" | "acreate_file" | "create_file" | "afile_content" | "file_content" | "create_fine_tuning_job" | "acreate_fine_tuning_job" | "create_video" | "acreate_video" | "avideo_retrieve" | "video_retrieve" | "avideo_content" | "video_content" | "video_remix" | "avideo_remix" | "video_list" | "avideo_list" | "video_retrieve_job" | "avideo_retrieve_job" | "video_delete" | "avideo_delete" | "video_create_character" | "avideo_create_character" | "video_get_character" | "avideo_get_character" | "video_edit" | "avideo_edit" | "video_extension" | "avideo_extension" | "vector_store_file_create" | "avector_store_file_create" | "vector_store_file_list" | "avector_store_file_list" | "vector_store_file_retrieve" | "avector_store_file_retrieve" | "vector_store_file_content" | "avector_store_file_content" | "vector_store_file_update" | "avector_store_file_update" | "vector_store_file_delete" | "avector_store_file_delete" | "vector_store_create" | "avector_store_create" | "vector_store_search" | "avector_store_search" | "create_container" | "acreate_container" | "list_containers" | "alist_containers" | "retrieve_container" | "aretrieve_container" | "delete_container" | "adelete_container" | "list_container_files" | "alist_container_files" | "upload_container_file" | "aupload_container_file" | "acancel_fine_tuning_job" | "cancel_fine_tuning_job" | "alist_fine_tuning_jobs" | "list_fine_tuning_jobs" | "aretrieve_fine_tuning_job" | "retrieve_fine_tuning_job" | "responses" | "aresponses" | "alist_input_items" | "llm_passthrough_route" | "allm_passthrough_route" | "generate_content" | "agenerate_content" | "generate_content_stream" | "agenerate_content_stream" | "ocr" | "aocr" | "call_mcp_tool" | "list_mcp_tools" | "asend_message" | "send_message" | "acreate_skill";
CallTypes: "embedding" | "aembedding" | "completion" | "acompletion" | "atext_completion" | "text_completion" | "image_generation" | "aimage_generation" | "image_edit" | "aimage_edit" | "moderation" | "amoderation" | "atranscription" | "transcription" | "aspeech" | "speech" | "rerank" | "arerank" | "search" | "asearch" | "_arealtime" | "_aresponses_websocket" | "create_batch" | "acreate_batch" | "aretrieve_batch" | "retrieve_batch" | "acancel_batch" | "cancel_batch" | "pass_through_endpoint" | "anthropic_messages" | "get_assistants" | "aget_assistants" | "create_assistants" | "acreate_assistants" | "delete_assistant" | "adelete_assistant" | "acreate_thread" | "create_thread" | "aget_thread" | "get_thread" | "a_add_message" | "add_message" | "aget_messages" | "get_messages" | "arun_thread" | "run_thread" | "arun_thread_stream" | "run_thread_stream" | "afile_retrieve" | "file_retrieve" | "afile_delete" | "file_delete" | "afile_list" | "file_list" | "acreate_file" | "create_file" | "afile_content" | "file_content" | "create_fine_tuning_job" | "acreate_fine_tuning_job" | "create_video" | "acreate_video" | "avideo_retrieve" | "video_retrieve" | "avideo_content" | "video_content" | "video_remix" | "avideo_remix" | "video_list" | "avideo_list" | "video_retrieve_job" | "avideo_retrieve_job" | "video_delete" | "avideo_delete" | "video_create_character" | "avideo_create_character" | "video_get_character" | "avideo_get_character" | "video_edit" | "avideo_edit" | "video_extension" | "avideo_extension" | "vector_store_file_create" | "avector_store_file_create" | "vector_store_file_list" | "avector_store_file_list" | "vector_store_file_retrieve" | "avector_store_file_retrieve" | "vector_store_file_content" | "avector_store_file_content" | "vector_store_file_update" | "avector_store_file_update" | "vector_store_file_delete" | "avector_store_file_delete" | "vector_store_create" | "avector_store_create" | "vector_store_search" | "avector_store_search" | "create_container" | "acreate_container" | "list_containers" | "alist_containers" | "retrieve_container" | "aretrieve_container" | "delete_container" | "adelete_container" | "list_container_files" | "alist_container_files" | "upload_container_file" | "aupload_container_file" | "create_sandbox" | "acreate_sandbox" | "delete_sandbox" | "adelete_sandbox" | "run_code" | "arun_code" | "code_interpreter_tool" | "acode_interpreter_tool" | "acancel_fine_tuning_job" | "cancel_fine_tuning_job" | "alist_fine_tuning_jobs" | "list_fine_tuning_jobs" | "aretrieve_fine_tuning_job" | "retrieve_fine_tuning_job" | "responses" | "aresponses" | "alist_input_items" | "llm_passthrough_route" | "allm_passthrough_route" | "generate_content" | "agenerate_content" | "generate_content_stream" | "agenerate_content_stream" | "ocr" | "aocr" | "call_mcp_tool" | "list_mcp_tools" | "asend_message" | "send_message" | "acreate_skill";
/** CallbackDelete */
CallbackDelete: {
/** Callback Name */