Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions docs/content/docs/cua/reference/agent-sdk/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ description: Python API reference for building computer-use agents
AUTO-GENERATED FILE - DO NOT EDIT DIRECTLY
Generated by: npx tsx scripts/docs-generators/python-sdk.ts
Source: libs/python/agent/agent
Version: 0.7.24
Version: 0.7.27
*/}

import { Callout } from 'fumadocs-ui/components/callout';
Expand All @@ -17,7 +17,7 @@ import { VersionHeader } from '@/components/version-selector';
<VersionHeader
versions={[{"version":"0.7","href":"/cua/reference/agent-sdk","isCurrent":true},{"version":"0.6","href":"/cua/reference/agent-sdk/v0.6/api","isCurrent":false},{"version":"0.5","href":"/cua/reference/agent-sdk/v0.5/api","isCurrent":false},{"version":"0.4","href":"/cua/reference/agent-sdk/v0.4/api","isCurrent":false},{"version":"0.3","href":"/cua/reference/agent-sdk/v0.3/api","isCurrent":false}]}
currentVersion="0.7"
fullVersion="0.7.24"
fullVersion="0.7.27"
packageName="cua-agent"
/>

Expand Down Expand Up @@ -1130,7 +1130,7 @@ Called when an LLM API call has completed.

*Inherits from: AsyncCallbackHandler*

Callback that captures errors and sends them to Sentry/OTEL.
Callback that captures errors and sends them to OTEL.

Should be added early in the callback chain to catch all errors.

Expand Down
4 changes: 2 additions & 2 deletions docs/content/docs/cua/reference/cli/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ description: Python API reference for the Cua command-line interface
AUTO-GENERATED FILE - DO NOT EDIT DIRECTLY
Generated by: npx tsx scripts/docs-generators/python-sdk.ts
Source: libs/python/cua-cli/cua_cli
Version: 0.1.0
Version: 0.1.5
*/}

import { Callout } from 'fumadocs-ui/components/callout';
Expand All @@ -17,7 +17,7 @@ import { VersionHeader } from '@/components/version-selector';
<VersionHeader
versions={[{"version":"0.1","href":"/cua/reference/cli","isCurrent":true}]}
currentVersion="0.1"
fullVersion="0.1.0"
fullVersion="0.1.5"
packageName="cua-cli"
/>

Expand Down
4 changes: 2 additions & 2 deletions docs/content/docs/cua/reference/computer-sdk/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ description: Python API reference for controlling virtual machines and computer
AUTO-GENERATED FILE - DO NOT EDIT DIRECTLY
Generated by: npx tsx scripts/docs-generators/python-sdk.ts
Source: libs/python/computer/computer
Version: 0.5.12
Version: 0.5.14
*/}

import { Callout } from 'fumadocs-ui/components/callout';
Expand All @@ -17,7 +17,7 @@ import { VersionHeader } from '@/components/version-selector';
<VersionHeader
versions={[{"version":"0.5","href":"/cua/reference/computer-sdk","isCurrent":true},{"version":"0.4","href":"/cua/reference/computer-sdk/v0.4/api","isCurrent":false},{"version":"0.3","href":"/cua/reference/computer-sdk/v0.3/api","isCurrent":false}]}
currentVersion="0.5"
fullVersion="0.5.12"
fullVersion="0.5.14"
packageName="cua-computer"
/>

Expand Down
6 changes: 3 additions & 3 deletions docs/content/docs/cuabench/reference/api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ description: Python API reference for the desktop automation benchmarking framew
AUTO-GENERATED FILE - DO NOT EDIT DIRECTLY
Generated by: npx tsx scripts/docs-generators/python-sdk.ts
Source: libs/cua-bench/cua_bench
Version: 0.2.3
Version: 0.2.4
*/}

import { Callout } from 'fumadocs-ui/components/callout';
Expand All @@ -17,7 +17,7 @@ import { VersionHeader } from '@/components/version-selector';
<VersionHeader
versions={[{"version":"0.2","href":"/cuabench/reference","isCurrent":true}]}
currentVersion="0.2"
fullVersion="0.2.3"
fullVersion="0.2.4"
packageName="cua-bench"
/>

Expand Down Expand Up @@ -3631,7 +3631,7 @@ Run a task with 2-container architecture.
#### TaskRunner.run_task_interactively

```python
async def run_task_interactively(self, env_type: str, golden_name: Optional[str] = None, env_path: Optional[Path] = None, task_index: int = 0, memory: str = '8G', cpus: str = '8', vnc_port: Optional[int] = None, api_port: Optional[int] = None, auto_allocate_ports: bool = True, cleanup_before: bool = True) -> tuple[str, str, callable, Optional[dict]]
async def run_task_interactively(self, env_type: str, golden_name: Optional[str] = None, env_path: Optional[Path] = None, task_index: int = 0, setup_config: Optional[dict] = None, memory: str = '8G', cpus: str = '8', vnc_port: Optional[int] = None, api_port: Optional[int] = None, auto_allocate_ports: bool = True, cleanup_before: bool = True) -> tuple[str, str, callable, Optional[dict]]
```

Start an environment container interactively (without agent).
Expand Down
19 changes: 19 additions & 0 deletions libs/python/agent/agent/adapters/cua_adapter.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import os
from typing import Any, AsyncIterator, Iterator

from core.http import cua_version_headers
from litellm import acompletion, completion
from litellm.llms.custom_llm import CustomLLM
from litellm.types.utils import GenericStreamingChunk, ModelResponse
Expand Down Expand Up @@ -73,6 +74,11 @@ def completion(self, *args, **kwargs) -> ModelResponse:
params["headers"] = kwargs["headers"]
del kwargs["headers"]

# Always include CUA version headers
version_hdrs = cua_version_headers()
if version_hdrs:
params["headers"] = {**version_hdrs, **params.get("headers", {})}

# Print dropped parameters
original_keys = set(kwargs.keys())
used_keys = set(params.keys()) # Only these are extracted from kwargs
Expand Down Expand Up @@ -130,6 +136,11 @@ async def acompletion(self, *args, **kwargs) -> ModelResponse:
params["headers"] = kwargs["headers"]
del kwargs["headers"]

# Always include CUA version headers
version_hdrs = cua_version_headers()
if version_hdrs:
params["headers"] = {**version_hdrs, **params.get("headers", {})}

# Print dropped parameters
original_keys = set(kwargs.keys())
used_keys = set(params.keys()) # Only these are extracted from kwargs
Expand Down Expand Up @@ -166,6 +177,10 @@ def streaming(self, *args, **kwargs) -> Iterator[GenericStreamingChunk]:
"stream": True,
}
)
# Always include CUA version headers
version_hdrs = cua_version_headers()
if version_hdrs:
params["headers"] = {**version_hdrs, **params.get("headers", {})}
# Yield chunks directly from LiteLLM's streaming generator
for chunk in completion(**params): # type: ignore
yield chunk # type: ignore
Expand All @@ -182,6 +197,10 @@ async def astreaming(self, *args, **kwargs) -> AsyncIterator[GenericStreamingChu
"stream": True,
}
)
# Always include CUA version headers
version_hdrs = cua_version_headers()
if version_hdrs:
params["headers"] = {**version_hdrs, **params.get("headers", {})}
stream = await acompletion(**params) # type: ignore
async for chunk in stream: # type: ignore
yield chunk # type: ignore
4 changes: 3 additions & 1 deletion libs/python/computer-server/computer_server/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,8 +270,10 @@ async def auth(self, container_name: str, api_key: str) -> bool:
logger.info(f"Authenticating with TryCUA API for container: {container_name}")

try:
from core.http import cua_version_headers

async with aiohttp.ClientSession() as session:
headers = {"Authorization": f"Bearer {api_key}"}
headers = {"Authorization": f"Bearer {api_key}", **cua_version_headers()}

async with session.get(
f"https://www.cua.ai/api/vm/auth?container_name={container_name}",
Expand Down
5 changes: 3 additions & 2 deletions libs/python/computer/computer/interface/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import aiohttp
import websockets
from core.http import cua_version_headers
from PIL import Image

from ..logger import Logger, LogLevel
Expand Down Expand Up @@ -708,7 +709,7 @@ async def playwright_exec(self, command: str, params: Optional[Dict] = None) ->
url = f"{protocol}://{self.ip_address}:{port}/playwright_exec"

payload = {"command": command, "params": params or {}}
headers = {"Content-Type": "application/json"}
headers = {"Content-Type": "application/json", **cua_version_headers()}
if self.api_key:
headers["X-API-Key"] = self.api_key
if self.vm_name:
Expand Down Expand Up @@ -947,7 +948,7 @@ async def _send_command_rest(
payload = {"command": command, "params": params or {}}

# Prepare headers
headers = {"Content-Type": "application/json"}
headers = {"Content-Type": "application/json", **cua_version_headers()}
if self.api_key:
headers["X-API-Key"] = self.api_key
if self.vm_name:
Expand Down
30 changes: 13 additions & 17 deletions libs/python/computer/computer/providers/cloud/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from urllib.parse import urlparse

import aiohttp
from core.http import cua_version_headers

DEFAULT_API_BASE = os.getenv("CUA_API_BASE", "https://api.cua.ai")

Expand Down Expand Up @@ -54,6 +55,13 @@ def __init__(
# Host caching dictionary: {vm_name: host_string}
self._host_cache: Dict[str, str] = {}

def _base_headers(self) -> Dict[str, str]:
return {
"Authorization": f"Bearer {self.api_key}",
"Accept": "application/json",
**cua_version_headers(),
}

@property
def provider_type(self) -> VMProviderType:
return VMProviderType.CLOUD
Expand All @@ -75,7 +83,7 @@ async def get_vm(self, name: str, storage: Optional[str] = None) -> Dict[str, An

# Query the API for authoritative VM info
url = f"{self.api_base}/v1/vms/{name}"
headers = {"Authorization": f"Bearer {self.api_key}", "Accept": "application/json"}
headers = self._base_headers()
try:
async with aiohttp.ClientSession() as session:
async with session.get(url, headers=headers) as resp:
Expand Down Expand Up @@ -112,10 +120,7 @@ async def get_vm(self, name: str, storage: Optional[str] = None) -> Dict[str, An

async def list_vms(self) -> ListVMsResponse:
url = f"{self.api_base}/v1/vms"
headers = {
"Authorization": f"Bearer {self.api_key}",
"Accept": "application/json",
}
headers = self._base_headers()
async with aiohttp.ClientSession() as session:
async with session.get(url, headers=headers) as resp:
if resp.status == 200:
Expand Down Expand Up @@ -175,10 +180,7 @@ async def run_vm(
) -> Dict[str, Any]:
"""Start a VM via public API. Returns a minimal status."""
url = f"{self.api_base}/v1/vms/{name}/start"
headers = {
"Authorization": f"Bearer {self.api_key}",
"Accept": "application/json",
}
headers = self._base_headers()
async with aiohttp.ClientSession() as session:
async with session.post(url, headers=headers) as resp:
if resp.status in (200, 201, 202, 204):
Expand All @@ -194,10 +196,7 @@ async def run_vm(
async def stop_vm(self, name: str, storage: Optional[str] = None) -> Dict[str, Any]:
"""Stop a VM via public API."""
url = f"{self.api_base}/v1/vms/{name}/stop"
headers = {
"Authorization": f"Bearer {self.api_key}",
"Accept": "application/json",
}
headers = self._base_headers()
async with aiohttp.ClientSession() as session:
async with session.post(url, headers=headers) as resp:
if resp.status in (200, 202):
Expand All @@ -220,10 +219,7 @@ async def stop_vm(self, name: str, storage: Optional[str] = None) -> Dict[str, A
async def restart_vm(self, name: str, storage: Optional[str] = None) -> Dict[str, Any]:
"""Restart a VM via public API."""
url = f"{self.api_base}/v1/vms/{name}/restart"
headers = {
"Authorization": f"Bearer {self.api_key}",
"Accept": "application/json",
}
headers = self._base_headers()
async with aiohttp.ClientSession() as session:
async with session.post(url, headers=headers) as resp:
if resp.status in (200, 202):
Expand Down
30 changes: 13 additions & 17 deletions libs/python/computer/computer/providers/cloud/providerv2.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import os

import aiohttp
from core.http import cua_version_headers

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for all absolute imports from core in the providers package
echo "=== Absolute core imports in providers package ==="
rg "from core\." libs/python/computer/computer/providers/ --type py -C 1

# Search for the cua_version_headers specifically
echo -e "\n=== All imports of cua_version_headers ==="
rg "cua_version_headers" libs/python/computer/ --type py -C 1

# Check the target file directly
echo -e "\n=== All imports in providerv2.py ==="
head -30 libs/python/computer/computer/providers/cloud/providerv2.py

Repository: trycua/cua

Length of output: 3171


🏁 Script executed:

#!/bin/bash
# Check project structure and setup files for package configuration
echo "=== Looking for setup.py or pyproject.toml ==="
find libs/python/computer -name "setup.py" -o -name "pyproject.toml" | head -10

# If found, check if they define core as a package
echo -e "\n=== Checking setup files for core package definition ==="
cat libs/python/computer/setup.py 2>/dev/null || echo "No setup.py found in computer/"
cat libs/python/computer/pyproject.toml 2>/dev/null || echo "No pyproject.toml found in computer/"

Repository: trycua/cua

Length of output: 1214


🏁 Script executed:

#!/bin/bash
# Check the structure of libs/python to understand namespace
echo "=== Package structure in libs/python ==="
ls -la libs/python/
echo -e "\n=== Contents of libs/python/computer ==="
ls -la libs/python/computer/
echo -e "\n=== Check if core is a sibling package ==="
ls -la libs/python/ | grep -E "^d"

Repository: trycua/cua

Length of output: 1905


Use relative imports for consistency with other local imports in this file.

The absolute from core.http import cua_version_headers is inconsistent with other intra-project imports like from ..base import ... and from ..types import .... This pattern appears in multiple files but still depends on core being on sys.path, making imports fragile to packaging restructures. Consider converting to a relative import or centralizing the core module handling across the codebase.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/python/computer/computer/providers/cloud/providerv2.py` at line 21,
Replace the absolute import "from core.http import cua_version_headers" with a
relative import to match the other intra-package imports; update the import in
providerv2.py to import cua_version_headers using the appropriate relative path
(similar to the existing "from ..base" and "from ..types" style) so the module
no longer depends on core being on sys.path and remains resilient to packaging
changes.


DEFAULT_API_BASE = os.getenv("CUA_API_BASE", "https://api.cua.ai")

Expand Down Expand Up @@ -48,6 +49,13 @@ def __init__(
self.verbose = verbose
self.api_base = (api_base or DEFAULT_API_BASE).rstrip("/")

def _base_headers(self) -> Dict[str, str]:
return {
"Authorization": f"Bearer {self.api_key}",
"Accept": "application/json",
**cua_version_headers(),
}

@property
def provider_type(self) -> VMProviderType:
return VMProviderType.CLOUDV2
Expand Down Expand Up @@ -91,7 +99,7 @@ async def get_vm(self, name: str, storage: Optional[str] = None) -> Dict[str, An

# Query the API for authoritative VM info
url = f"{self.api_base}/v1/vms/{name}"
headers = {"Authorization": f"Bearer {self.api_key}", "Accept": "application/json"}
headers = self._base_headers()
try:
async with aiohttp.ClientSession() as session:
async with session.get(url, headers=headers) as resp:
Expand Down Expand Up @@ -125,10 +133,7 @@ async def get_vm(self, name: str, storage: Optional[str] = None) -> Dict[str, An

async def list_vms(self) -> ListVMsResponse:
url = f"{self.api_base}/v1/vms"
headers = {
"Authorization": f"Bearer {self.api_key}",
"Accept": "application/json",
}
headers = self._base_headers()
async with aiohttp.ClientSession() as session:
async with session.get(url, headers=headers) as resp:
if resp.status == 200:
Expand Down Expand Up @@ -179,10 +184,7 @@ async def run_vm(
) -> Dict[str, Any]:
"""Start a VM via public API. Returns a minimal status."""
url = f"{self.api_base}/v1/vms/{name}/start"
headers = {
"Authorization": f"Bearer {self.api_key}",
"Accept": "application/json",
}
headers = self._base_headers()
async with aiohttp.ClientSession() as session:
async with session.post(url, headers=headers) as resp:
if resp.status in (200, 201, 202, 204):
Expand All @@ -198,10 +200,7 @@ async def run_vm(
async def stop_vm(self, name: str, storage: Optional[str] = None) -> Dict[str, Any]:
"""Stop a VM via public API."""
url = f"{self.api_base}/v1/vms/{name}/stop"
headers = {
"Authorization": f"Bearer {self.api_key}",
"Accept": "application/json",
}
headers = self._base_headers()
async with aiohttp.ClientSession() as session:
async with session.post(url, headers=headers) as resp:
if resp.status in (200, 202):
Expand All @@ -223,10 +222,7 @@ async def stop_vm(self, name: str, storage: Optional[str] = None) -> Dict[str, A
async def restart_vm(self, name: str, storage: Optional[str] = None) -> Dict[str, Any]:
"""Restart a VM via public API."""
url = f"{self.api_base}/v1/vms/{name}/restart"
headers = {
"Authorization": f"Bearer {self.api_key}",
"Accept": "application/json",
}
headers = self._base_headers()
async with aiohttp.ClientSession() as session:
async with session.post(url, headers=headers) as resp:
if resp.status in (200, 202):
Expand Down
2 changes: 2 additions & 0 deletions libs/python/core/core/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Core functionality shared across Cua components."""

__version__ = "0.1.8"

from core.http import CUA_CLIENT_VERSION_HEADER, cua_version_headers
35 changes: 35 additions & 0 deletions libs/python/core/core/http.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""Shared HTTP utilities for CUA SDK requests."""

from functools import lru_cache

CUA_CLIENT_VERSION_HEADER = "X-Cua-Client-Version"

# CUA packages whose versions are included in the header value.
_CUA_PACKAGES = ("cua-agent", "cua-computer", "cua-core")


@lru_cache(maxsize=1)
def _build_version_string() -> str:
"""Return a composite version string like ``agent:0.4.0 computer:0.1.0 core:0.1.8``."""
from importlib.metadata import PackageNotFoundError, version

parts: list[str] = []
for pkg in _CUA_PACKAGES:
try:
short = pkg.removeprefix("cua-")
parts.append(f"{short}:{version(pkg)}")
except PackageNotFoundError:
continue
return " ".join(parts)


def cua_version_headers() -> dict[str, str]:
"""Return headers dict containing the CUA client version header.

Only installed CUA packages are included. If none are found the dict is
empty so it is always safe to unpack with ``**cua_version_headers()``.
"""
value = _build_version_string()
if not value:
return {}
return {CUA_CLIENT_VERSION_HEADER: value}
2 changes: 2 additions & 0 deletions libs/python/cua-cli/cua_cli/api/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from urllib.parse import quote

import aiohttp
from core.http import cua_version_headers
from cua_cli.auth.store import require_api_key

DEFAULT_API_BASE = "https://api.cua.ai"
Expand All @@ -28,6 +29,7 @@ def _headers(self) -> dict[str, str]:
return {
"Authorization": f"Bearer {self.api_key}",
"Accept": "application/json",
**cua_version_headers(),
}

async def _request(
Expand Down
Loading
Loading