샌드박스 / 코드 실행
샌드박스 / 코드 실행 (Sandbox / Code Execution)
모델이 생성한 코드를 격리된 샌드박스에서 실행하고 그 출력을 돌려받는 기능이에요. API는 제공자에 구애받지 않으며, e2b와 opensandbox 백엔드를 추가 SDK 의존성 없이 HTTPS로 직접 통신해 사용해요.
출처: 문서
본문
모델이 생성한 코드를 격리된 샌드박스 안에서 실행하고 그 출력을 돌려받아요. API는 제공자에 구애받지 않으며, e2b와 opensandbox가 지원 백엔드로, 추가 SDK 의존성 없이 HTTPS로 직접 통신해요.
| 기능 | 지원 |
|---|---|
| 지원 제공자 | e2b, opensandbox |
| 비용 추적 | 패스스루 (샌드박스 요금은 제공자에 남음) |
| 로깅 | litellm.asearch가 사용하는 표준 @client 로깅 경로로 처리 |
| 프록시 엔드포인트 | /v1/responses 및 /v1/chat/completions의 코드 인터프리터 인터셉터를 통해 (아래 참고); 아직 독립 /v1/sandbox는 없음 |
tip
code는 실행 가능한 문자열이에요. 언어 조절 장치나 발명된 결과 스키마는 없고,CodeExecutionResult는 샌드박스 자체 출력(stdout, stderr, base64 차트 같은 결과, 오류 이름/값/트레이스백, 실행 횟수)의 패스스루예요.
코드 인터프리터 인터셉터
OpenAI의 code_interpreter 도구를 OpenAI 컨테이너 대신 샌드박스로 라우팅해요. /v1/responses와 /v1/chat/completions 모두에서 동작하며, 클라이언트 요청은 평범한 OpenAI 형태로 유지돼요. 채팅 경로에서는 네이티브 {"type": "code_interpreter"} 도구가 litellm_code_execution 함수 도구로 재작성되고, LiteLLM이 생성된 코드를 샌드박스에서 실행해 결과를 role: tool 메시지로 추가한 뒤 최종 답이 나올 때까지 루프를 계속해요.
SDK
샌드박스 도구를 등록하고, 인터셉터를 콜백으로 설치하고, code_interpreter 도구를 그대로 둔 채 litellm.aresponses(또는 litellm.acompletion)를 호출해요.
- Responses API
- Chat Completions
sandbox_interceptor.py
import os, litellm
from litellm.sandbox.sandbox_tools import register_sandbox_tools
from litellm.integrations.code_interpreter_interception.handler import (
CodeInterpreterInterceptionLogger,
)
os.environ["E2B_API_KEY"] = "e2b_..."
os.environ["OPENAI_API_KEY"] = "sk-..."
register_sandbox_tools([
{
"sandbox_tool_name": "my-e2b",
"litellm_params": {
"sandbox_provider": "e2b",
"api_key": "os.environ/E2B_API_KEY",
},
}
])
litellm.callbacks = [
CodeInterpreterInterceptionLogger(
sandbox_tool_name="my-e2b",
)
]
response = await litellm.aresponses(
model="openai/gpt-5.6-terra",
tools=[{"type": "code_interpreter", "container": {"type": "auto"}}],
input="Product of first 6 primes. Just the number.",
)
print(response.output_text)
sandbox_interceptor_chat.py
import os, litellm
from litellm.sandbox.sandbox_tools import register_sandbox_tools
from litellm.integrations.code_interpreter_interception.handler import (
CodeInterpreterInterceptionLogger,
)
os.environ["E2B_API_KEY"] = "e2b_..."
os.environ["OPENAI_API_KEY"] = "sk-..."
register_sandbox_tools([
{
"sandbox_tool_name": "my-e2b",
"litellm_params": {
"sandbox_provider": "e2b",
"api_key": "os.environ/E2B_API_KEY",
},
}
])
litellm.callbacks = [
CodeInterpreterInterceptionLogger(
sandbox_tool_name="my-e2b",
)
]
response = await litellm.acompletion(
model="openai/gpt-5.6-luna",
messages=[{"role": "user", "content": "Product of first 6 primes. Just the number."}],
tools=[{"type": "code_interpreter", "container": {"type": "auto"}}],
max_agentic_loops=4,
)
print(response.choices[0].message.content)
max_agentic_loops는 모델이 만들어낸 것을 반환하기 전에 LiteLLM이 실행할 샌드박스 왕복 횟수를 제한해요. 루프는 반복되는 도구 호출 지문에서도 조기 종료돼요. 기본값은 보수적이며, 더 깊은 연쇄 실행이 필요하면 올리세요.
프록시 설정
1. 키 설정
export E2B_API_KEY="e2b_..."
export OPENAI_API_KEY="sk-..."
2. config.yaml 작성
config.yaml
model_list:
- model_name: gpt-5.6-terra
litellm_params:
model: openai/gpt-5.6-terra
api_key: os.environ/OPENAI_API_KEY
sandbox_tools:
- sandbox_tool_name: my-e2b
litellm_params:
sandbox_provider: e2b
api_key: os.environ/E2B_API_KEY
litellm_settings:
callbacks: ["code_interpreter_interception"]
code_interpreter_interception_params:
sandbox_tool_name: my-e2b
3. 프록시 시작
litellm --config /path/to/config.yaml
4. 프록시 호출
- Responses (curl)
- Responses (OpenAI SDK)
- Chat Completions (curl)
- Chat Completions (OpenAI SDK)
curl -s "http://localhost:4000/v1/responses" \
-H "Authorization: Bearer ***" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-5.6-terra",
"tools": [{"type": "code_interpreter", "container": {"type": "auto"}}],
"input": "Product of first 6 primes. Just the number."
}'
from openai import OpenAI
client = OpenAI(api_key="sk-<your-litellm-api-key>", base_url="http://localhost:4000/v1")
response = client.responses.create(
model="gpt-5.6-terra",
tools=[{"type": "code_interpreter", "container": {"type": "auto"}}],
input="Product of first 6 primes. Just the number.",
)
print(response.output_text)
curl -s "http://localhost:4000/v1/chat/completions" \
-H "Authorization: Bearer ***" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-5.6-luna",
"messages": [
{"role": "user", "content": "Product of first 6 primes. Just the number."}
],
"tools": [{"type": "code_interpreter", "container": {"type": "auto"}}]
}'
from openai import OpenAI
client = OpenAI(api_key="sk-<your-litellm-api-key>", base_url="http://localhost:4000/v1")
response = client.chat.completions.create(
model="gpt-5.6-luna",
messages=[{"role": "user", "content": "Product of first 6 primes. Just the number."}],
tools=[{"type": "code_interpreter", "container": {"type": "auto"}}],
)
print(response.choices[0].message.content)
Responses 경로에서 결과에는 샌드박스 id를 감싸는 cntr_* container_id가 있는 code_interpreter_call 항목이 포함돼요. Chat Completions 경로에서는 도구 호출이 실행된 코드를 든 litellm_code_execution 함수 호출로 나타나고, 그 뒤로 stdout을 담은 role: tool 메시지가 이어지며, 모델의 최종 답은 다음 어시스턴트 메시지에서 나와요.
e2b 대신 OpenSandbox에서 실행하려면 sandbox_tools 항목을 바꾸세요:
sandbox_tools:
- sandbox_tool_name: my-opensandbox
litellm_params:
sandbox_provider: opensandbox
api_base: os.environ/OPEN_SANDBOX_API_BASE
api_key: os.environ/OPEN_SANDBOX_API_KEY
스티키 세션
기본적으로 각 요청은 에이전틱 루프가 끝나면 삭제되는 새 샌드박스를 띄워요. 순차 요청 간 같은 샌드박스를 재사용하려면 metadata.session_id를 전달해서 한 턴에서 정의한 변수, 임포트, 파일이 다음 턴에도 살아있게 해요.
- curl
- OpenAI SDK
# First request: define x
curl -s "http://localhost:4000/v1/responses" \
-H "Authorization: Bearer ***" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-5.6-terra",
"tools": [{"type": "code_interpreter", "container": {"type": "auto"}}],
"input": "Set x = 42 and confirm.",
"metadata": {"session_id": "chat-abc-123"}
}'
# Second request: same session_id, x is still there
curl -s "http://localhost:4000/v1/responses" \
-H "Authorization: Bearer ***" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-5.6-terra",
"tools": [{"type": "code_interpreter", "container": {"type": "auto"}}],
"input": "Print x + 1.",
"metadata": {"session_id": "chat-abc-123"}
}'
from openai import OpenAI
client = OpenAI(api_key="sk-<your-litellm-api-key>", base_url="http://localhost:4000/v1")
client.responses.create(
model="gpt-5.6-terra",
tools=[{"type": "code_interpreter", "container": {"type": "auto"}}],
input="Set x = 42 and confirm.",
extra_body={"metadata": {"session_id": "chat-abc-123"}},
)
# Same session_id reuses the same e2b container, so x is still defined
followup = client.responses.create(
model="gpt-5.6-terra",
tools=[{"type": "code_interpreter", "container": {"type": "auto"}}],
input="Print x + 1.",
extra_body={"metadata": {"session_id": "chat-abc-123"}},
)
print(followup.output_text)
테넌트 간 격리는 클라이언트가 제공한 session_id를 프록시가 발행한 user_api_key_hash와 결합해 캐시 키({hash}:{session_id})를 만들어 강제되므로, 같은 session_id를 보내는 두 API 키가 샌드박스를 공유하지 않아요. 각 API 키는 활성 세션 범위 샌드박스 10개로 제한돼요. 새 세션이 한도를 초과하면 그 키에서 가장 덜 최근에 사용된 세션이 퇴거되고 샌드박스가 삭제돼요. 활성 세션은 TTL이 접근할 때마다 재설정되므로 대화 중간에 만료되지 않아요. session_id를 생략하면 원래의 일회용 요청별 동작을 유지해요.
참고 사항
응답 형태는 OpenAI 네이티브 code_interpreter_call과 일치해요. stream: true가 동작해요. 강제된 tool_choice: {"type":"code_interpreter"}는 자동으로 재작성돼요. 샌드박스는 기본적으로 요청별로 일회용이며, metadata.session_id가 설정되면 스티키해요(위 참고). 동시 요청은 서버 발행 캐시 키로 격리돼요. sandbox_tools에서 도구를 제거하면 리로드 시 해당 자격 증명이 지워져요. v0는 아직 파일 업로드/다운로드를 지원하지 않아요.
직접 샌드박스 SDK
모델 없이 샌드박스를 직접 구동하고 싶다면, 같은 제공자를 일반 Python 헬퍼로 노출해요.
Quick start (일회용)
가장 빠른 경로는 acode_interpreter_tool이에요. 샌드박스를 만들고, 코드를 실행하고, finally 블록에서 샌드박스를 삭제해서 예외가 발생해도 정리되게 해요.
Ephemeral code execution
import asyncio, os, litellm
os.environ["E2B_API_KEY"] = "e2b_..."
async def main():
result = await litellm.acode_interpreter_tool(
provider="e2b",
code="print(sum(range(10)))",
)
print(result.stdout) # '45\n'
print(result.error) # None
asyncio.run(main())
샌드박스 안에서 코드가 예외를 일으키면 오류는 Python 예외가 아니라 result.error에 나타나므로, 샌드박스 수준의 ZeroDivisionError가 호출자를 크래시시키지 않아요:
result = await litellm.acode_interpreter_tool(provider="e2b", code="1/0")
result.error["name"] # 'ZeroDivisionError'
result.error["value"] # 'division by zero'
result.error["traceback"] # full traceback string
저수준 생애주기
여러 arun_code 호출에 걸쳐 샌드박스를 재사용하려면 acreate_sandbox, arun_code, adelete_sandbox로 생애주기를 직접 다룰 수 있어요. 저수준 이름은 의도적으로 샌드박스 범위로 한정되어 있어, 관련 없고 그대로 두는 기존 OpenAI Containers API(litellm.create_container)와 충돌하지 않아요.
Manual sandbox lifecycle
import asyncio, os, litellm
os.environ["E2B_API_KEY"] = "e2b_..."
async def main():
container = await litellm.acreate_sandbox(provider="e2b")
try:
first = await litellm.arun_code(
provider="e2b", container=container, code="x = 6 * 7\nprint(x)",
)
print(first.stdout) # '42\n'
second = await litellm.arun_code(
provider="e2b", container=container, code="print(x + 1)",
)
print(second.stdout) # '43\n' (state persists inside the container)
finally:
await litellm.adelete_sandbox(provider="e2b", container=container)
asyncio.run(main())
arun_code와 adelete_sandbox는 acreate_sandbox가 반환한 ContainerHandle 또는 단순 샌드박스 id 문자열을 받을 수 있어서, id를 프로세스 간 유지하고 나중에 샌드박스를 다시 가져올 수 있어요.
파라미터
acode_interpreter_tool
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
provider |
string | 예 | 샌드박스 제공자 슬러그. "e2b", "opensandbox" 중 하나 |
code |
string | 예 | 샌드박스로 바로 전달되는 실행 가능한 문자열 |
template |
string | 아니요 | 제공자 템플릿 id. 기본값은 e2b의 code-interpreter-v1 |
timeout |
int | 아니요 | 샌드박스 수명(초). 기본값 300 |
api_key |
string | 아니요 | 환경 변수 조회를 재정의 |
api_base |
string | 아니요 | 제공자의 기본 호스트를 재정의. 셀프 호스팅 샌드박스를 클러스터 URL로 가리킬 때 사용 |
acreate_sandbox
위와 같은 형태에서 code가 없고, 이그레스를 제어하는 백엔드를 위한 allow_internet_access: bool = True가 추가돼요.
arun_code와 adelete_sandbox
provider, container(ContainerHandle 또는 샌드박스 id 문자열), 선택적 api_key/api_base. arun_code는 code도 받아요.
응답: CodeExecutionResult
반환 형태는 샌드박스가 내보낸 것을 보존하는 얇은 pydantic 모델이에요.
| 필드 | 타입 | 설명 |
|---|---|---|
stdout |
string | 캡처된 stdout. 아무것도 출력되지 않았으면 빈 문자열 |
stderr |
string | 캡처된 stderr |
results |
list[dict] | base64 PNG 차트 같은 풍부한 출력; 변경 없이 패스스루 |
error |
dict | None | 샌드박스 코드가 예외를 일으켰을 때 {name, value, traceback}, 아니면 None |
execution_count |
int | None | 실행을 위한 Jupyter 스타일 셀 카운터 |
object |
string | 항상 "code_execution" |
제공자 설정
e2b
E2B_API_KEY를 설정하거나(또는 호출별 api_key=... 전달) 사용해요. 기본값: 템플릿 code-interpreter-v1, 샌드박스 타임아웃 300초, 인터넷 접근 켜짐. 게시한 커스텀 e2b 템플릿을 쓰려면 template을 재정의해요.
result = await litellm.acode_interpreter_tool(
provider="e2b",
code="...",
template="my-org/custom-template",
timeout=120,
)
내부적으로 호출은 e2b의 REST API로 직접 간다: 생성은 POST api.e2b.app/sandboxes, 실행은 포트 49999의 샌드박스별 호스트로 스트리밍 NDJSON POST, 삭제는 DELETE api.e2b.app/sandboxes/{id}.
opensandbox
셀프 호스팅 코드 실행에는 OpenSandbox를 사용해요. OPEN_SANDBOX_API_BASE를 설정해(또는 호출별 api_base=... 전달) 서버를 가리켜요. localhost 폴백은 없어요. OPEN_SANDBOX_API_KEY는 선택이고, 로컬 no-auth 서버에서는 비워 두면 돼요. 샌드박스는 기본적으로 이그레스가 거부된 채 생성되며, 열려면 allow_internet_access=True 또는 명시적 network_policy를 전달해요.
import os, litellm
os.environ["OPEN_SANDBOX_API_BASE"] = "http://127.0.0.1:8080/v1"
os.environ["OPEN_SANDBOX_API_KEY"] = "" # optional for local no-auth servers
result = await litellm.acode_interpreter_tool(
provider="opensandbox",
code="print(sum(range(10)))",
)
print(result.stdout) # '45\n'
제공자는 OpenSandbox의 REST 생애주기를 직접 구동해요: 생성은 POST /v1/sandboxes, 샌드박스별 execd 엔드포인트를 해석해 /code SSE를 CodeExecutionResult로 스트리밍한 뒤 DELETE /v1/sandboxes/{id}로 정리해요. 다른 기본값(템플릿, 엔트리포인트, 언어, 폴링 간격, execd 포트, 기본 네트워크 정책, 출력 상한)은 litellm/constants.py에 리터럴로 존재해요.