샌드박스 / 코드 실행

샌드박스 / 코드 실행 (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_codeadelete_sandboxacreate_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_codeadelete_sandbox

provider, container(ContainerHandle 또는 샌드박스 id 문자열), 선택적 api_key/api_base. arun_codecode도 받아요.

응답: 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에 리터럴로 존재해요.

더 알아보기 (Learn more)