remove Vercel AI Gateway and Vercel Sandbox (#33067)

* remove Vercel AI Gateway provider and Vercel Sandbox terminal backend

Both Vercel-hosted integrations are removed end-to-end. Users on the AI
Gateway should switch to OpenRouter or one of the other aggregators
(Nous Portal, Kilo Code). Users on the Vercel Sandbox backend should
switch to Docker, Modal, Daytona, or SSH.

What's removed:
- `plugins/model-providers/ai-gateway/` provider plugin
- `hermes_cli/vercel_auth.py` Vercel-Sandbox auth helper
- `tools/environments/vercel_sandbox.py` terminal backend
- `ai-gateway` provider wiring across auth, doctor, setup, models,
  config, status, providers, main, web_server, model_normalize, dump
- `vercel_sandbox` backend wiring across terminal_tool, file_tools,
  code_execution_tool, file_operations, approval, skills_tool,
  environments/local, credential_files, lazy_deps, prompt_builder,
  cli, gateway/run
- `AI_GATEWAY_BASE_URL` constant, `_AI_GATEWAY_HEADERS` auxiliary-client
  header set, run_agent base-URL header/reasoning special-cases
- `[vercel]` pyproject extra and `vercel`/`vercel-workers` from uv.lock
- env vars: `AI_GATEWAY_API_KEY`, `AI_GATEWAY_BASE_URL`, `VERCEL_TOKEN`,
  `VERCEL_PROJECT_ID`, `VERCEL_TEAM_ID`, `VERCEL_OIDC_TOKEN`,
  `TERMINAL_VERCEL_RUNTIME`
- Tests: deletes test_ai_gateway_models.py and
  test_vercel_sandbox_environment.py; scrubs references across 23
  surviving test files (no entire tests deleted unless they were
  dedicated to AI Gateway / Sandbox)
- Docs: provider tables, env-var reference, setup guides, security
  notes, tool config, terminal-backend tables — English plus zh-Hans
  i18n parity
- `hermes-agent` skill: provider table entry and remote-backend list

What stays (intentional):
- `popular-web-designs/templates/vercel.md` — CSS design reference,
  unrelated to Vercel-the-AI-product
- `x-vercel-id` in `stream_diag.py` headers — generic Vercel CDN
  response header, useful diag signal on any Vercel-hosted endpoint
- `vercel-labs/agent-browser` URL in browser config — lightpanda
  browser project, different OSS effort
- `userStories.json` historical contributor entry mentioning Vercel
  Sandbox — archive, not active docs

Validation:
- 1153 tests in the 22 targeted files pass (`scripts/run_tests.sh`)
- Full repo `py_compile` clean
- Live import of every touched module + invariant check (no
  `ai-gateway` in `PROVIDER_REGISTRY`, no `_AI_GATEWAY_HEADERS`, no
  `vercel_sandbox` in `_REMOTE_TERMINAL_BACKENDS`)

* test: convert profile-count check from change-detector to invariant

The hardcoded "== 34" assertion broke when ai-gateway was removed.
Per AGENTS.md change-detector-test guidance, assert the relationship
(registry count >= number of plugin dirs) instead of a literal count.
Counts shift when providers are added/removed; that's expected.
This commit is contained in:
Teknium 2026-05-27 00:43:32 -07:00 committed by GitHub
parent cb38ce28cb
commit febc4cfec0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
95 changed files with 111 additions and 3088 deletions

View file

@ -3,18 +3,16 @@
Terminal Tool Module
A terminal tool that executes commands in local, Docker, Modal, SSH,
Singularity, Daytona, and Vercel Sandbox environments. Supports local
execution, containerized backends, and cloud sandboxes, including managed
Modal mode.
Singularity, and Daytona environments. Supports local execution,
containerized backends, and cloud sandboxes, including managed Modal mode.
Environment Selection (via TERMINAL_ENV environment variable):
Supported environments:
- "local": Execute directly on the host machine (default, fastest)
- "docker": Execute in Docker containers (isolated, requires Docker)
- "modal": Execute in Modal cloud sandboxes (direct Modal or managed gateway)
- "vercel_sandbox": Execute in Vercel Sandbox cloud sandboxes
Features:
- Multiple execution backends (local, docker, modal, vercel_sandbox)
- Multiple execution backends (local, docker, modal)
- Background task support
- VM/container lifecycle management
- Automatic cleanup after inactivity
@ -119,68 +117,6 @@ DISK_USAGE_WARNING_THRESHOLD_GB = _safe_parse_import_env(
float,
"number",
)
_VERCEL_SANDBOX_DEFAULT_CWD = "/vercel/sandbox"
_SUPPORTED_VERCEL_RUNTIMES = ("node24", "node22", "python3.13")
def _is_supported_vercel_runtime(runtime: str) -> bool:
return not runtime or runtime in _SUPPORTED_VERCEL_RUNTIMES
def _check_vercel_sandbox_requirements(config: dict[str, Any]) -> bool:
"""Validate Vercel Sandbox terminal backend requirements."""
runtime = (config.get("vercel_runtime") or "").strip()
if not _is_supported_vercel_runtime(runtime):
supported = ", ".join(_SUPPORTED_VERCEL_RUNTIMES)
logger.error(
"Vercel Sandbox runtime %r is not supported. "
"Set TERMINAL_VERCEL_RUNTIME to one of: %s.",
runtime,
supported,
)
return False
disk = config.get("container_disk", 51200)
if disk not in {0, 51200}:
logger.error(
"Vercel Sandbox does not support custom TERMINAL_CONTAINER_DISK=%s. "
"Use the default shared setting (51200 MB).",
disk,
)
return False
if importlib.util.find_spec("vercel") is None:
logger.error(
"vercel is required for the Vercel Sandbox terminal backend: pip install vercel"
)
return False
has_oidc = bool(os.getenv("VERCEL_OIDC_TOKEN"))
has_token = bool(os.getenv("VERCEL_TOKEN"))
has_project = bool(os.getenv("VERCEL_PROJECT_ID"))
has_team = bool(os.getenv("VERCEL_TEAM_ID"))
if has_oidc:
return True
if has_token or has_project or has_team:
if has_token and has_project and has_team:
return True
logger.error(
"Vercel Sandbox backend selected with token auth, but "
"VERCEL_TOKEN, VERCEL_PROJECT_ID, and VERCEL_TEAM_ID must all "
"be set together. VERCEL_OIDC_TOKEN is supported for one-off "
"local development only."
)
return False
logger.error(
"Vercel Sandbox backend selected but no supported auth configuration "
"was found. Set VERCEL_TOKEN, VERCEL_PROJECT_ID, and VERCEL_TEAM_ID "
"for normal use. VERCEL_OIDC_TOKEN is supported for one-off local "
"development only."
)
return False
def _check_disk_usage_warning():
@ -837,10 +773,9 @@ def _transform_sudo_command(command: str | None) -> tuple[str | None, str | None
should prepend sudo_stdin to their stdin_data and pass the merged bytes to
Popen's stdin pipe.
Callers that cannot pipe subprocess stdin (modal, daytona,
vercel_sandbox) must embed the password in the command string
themselves; see their execute() methods for how they handle the
non-None sudo_stdin case.
Callers that cannot pipe subprocess stdin (modal, daytona) must embed
the password in the command string themselves; see their execute()
methods for how they handle the non-None sudo_stdin case.
If SUDO_PASSWORD is not set and in interactive mode (HERMES_INTERACTIVE=1):
Prompts user for password with 45s timeout, caches for session.
@ -1015,14 +950,12 @@ def _get_env_config() -> Dict[str, Any]:
mount_docker_cwd = os.getenv("TERMINAL_DOCKER_MOUNT_CWD_TO_WORKSPACE", "false").lower() in {"true", "1", "yes"}
# Default cwd: local uses the host's current directory, ssh uses the
# remote home, Vercel uses its documented workspace root, and everything
# else starts in the backend's default root-like cwd.
# remote home, and everything else starts in the backend's default
# root-like cwd.
if env_type == "local":
default_cwd = os.getcwd()
elif env_type == "ssh":
default_cwd = "~"
elif env_type == "vercel_sandbox":
default_cwd = _VERCEL_SANDBOX_DEFAULT_CWD
else:
default_cwd = "/root"
@ -1044,7 +977,7 @@ def _get_env_config() -> Dict[str, Any]:
):
host_cwd = candidate
cwd = "/workspace"
elif env_type in {"modal", "docker", "singularity", "daytona", "vercel_sandbox"} and cwd:
elif env_type in {"modal", "docker", "singularity", "daytona"} and cwd:
# Host paths and relative paths that won't work inside containers
is_host_path = any(cwd.startswith(p) for p in host_prefixes)
is_relative = not os.path.isabs(cwd) # e.g. "." or "src/"
@ -1062,7 +995,6 @@ def _get_env_config() -> Dict[str, Any]:
"singularity_image": os.getenv("TERMINAL_SINGULARITY_IMAGE", f"docker://{default_image}"),
"modal_image": os.getenv("TERMINAL_MODAL_IMAGE", default_image),
"daytona_image": os.getenv("TERMINAL_DAYTONA_IMAGE", default_image),
"vercel_runtime": os.getenv("TERMINAL_VERCEL_RUNTIME", "").strip(),
"cwd": cwd,
"host_cwd": host_cwd,
"docker_mount_cwd_to_workspace": mount_docker_cwd,
@ -1082,7 +1014,7 @@ def _get_env_config() -> Dict[str, Any]:
).lower() in {"true", "1", "yes"},
"local_persistent": os.getenv("TERMINAL_LOCAL_PERSISTENT", "false").lower() in {"true", "1", "yes"},
# Container resource config (applies to docker, singularity, modal,
# daytona, and vercel_sandbox -- ignored for local/ssh)
# daytona -- ignored for local/ssh)
"container_cpu": _parse_env_var("TERMINAL_CONTAINER_CPU", "1", float, "number"),
"container_memory": _parse_env_var("TERMINAL_CONTAINER_MEMORY", "5120"), # MB (default 5GB)
"container_disk": _parse_env_var("TERMINAL_CONTAINER_DISK", "51200"), # MB (default 50GB)
@ -1113,8 +1045,8 @@ def _create_environment(env_type: str, image: str, cwd: str, timeout: int,
Args:
env_type: One of "local", "docker", "singularity", "modal",
"daytona", "vercel_sandbox", "ssh"
image: Docker/Singularity/Modal image name (ignored for local/ssh/vercel)
"daytona", "ssh"
image: Docker/Singularity/Modal image name (ignored for local/ssh)
cwd: Working directory
timeout: Default command timeout
ssh_config: SSH connection config (for env_type="ssh")
@ -1220,21 +1152,6 @@ def _create_environment(env_type: str, image: str, cwd: str, timeout: int,
persistent_filesystem=persistent, task_id=task_id,
)
elif env_type == "vercel_sandbox":
from tools.environments.vercel_sandbox import (
VercelSandboxEnvironment as _VercelSandboxEnvironment,
)
return _VercelSandboxEnvironment(
runtime=cc.get("vercel_runtime") or None,
cwd=cwd,
timeout=timeout,
cpu=cpu,
memory=memory,
disk=disk,
persistent_filesystem=persistent,
task_id=task_id,
)
elif env_type == "ssh":
if not ssh_config or not ssh_config.get("host") or not ssh_config.get("user"):
raise ValueError("SSH environment requires ssh_host and ssh_user to be configured")
@ -1250,7 +1167,7 @@ def _create_environment(env_type: str, image: str, cwd: str, timeout: int,
else:
raise ValueError(
f"Unknown environment type: {env_type}. Use 'local', 'docker', "
f"'singularity', 'modal', 'daytona', 'vercel_sandbox', or 'ssh'"
f"'singularity', 'modal', 'daytona', or 'ssh'"
)
@ -1809,14 +1726,13 @@ def terminal_tool(
}
container_config = None
if env_type in {"docker", "singularity", "modal", "daytona", "vercel_sandbox"}:
if env_type in {"docker", "singularity", "modal", "daytona"}:
container_config = {
"container_cpu": config.get("container_cpu", 1),
"container_memory": config.get("container_memory", 5120),
"container_disk": config.get("container_disk", 51200),
"container_persistent": config.get("container_persistent", True),
"modal_mode": config.get("modal_mode", "auto"),
"vercel_runtime": config.get("vercel_runtime", ""),
"docker_volumes": config.get("docker_volumes", []),
"docker_mount_cwd_to_workspace": config.get("docker_mount_cwd_to_workspace", False),
"docker_forward_env": config.get("docker_forward_env", []),
@ -2265,9 +2181,6 @@ def check_terminal_requirements() -> bool:
return True
elif env_type == "vercel_sandbox":
return _check_vercel_sandbox_requirements(config)
elif env_type == "daytona":
from daytona import Daytona # noqa: F401 — SDK presence check
return os.getenv("DAYTONA_API_KEY") is not None
@ -2275,7 +2188,7 @@ def check_terminal_requirements() -> bool:
else:
logger.error(
"Unknown TERMINAL_ENV '%s'. Use one of: local, docker, singularity, "
"modal, daytona, vercel_sandbox, ssh.",
"modal, daytona, ssh.",
env_type,
)
return False
@ -2318,7 +2231,7 @@ if __name__ == "__main__":
print(
" TERMINAL_ENV: "
f"{os.getenv('TERMINAL_ENV', 'local')} "
"(local/docker/singularity/modal/daytona/vercel_sandbox/ssh)"
"(local/docker/singularity/modal/daytona/ssh)"
)
print(f" TERMINAL_DOCKER_IMAGE: {os.getenv('TERMINAL_DOCKER_IMAGE', default_img)}")
print(f" TERMINAL_SINGULARITY_IMAGE: {os.getenv('TERMINAL_SINGULARITY_IMAGE', f'docker://{default_img}')}")