diff --git a/docs/content/docs/cua/reference/agent-sdk/index.mdx b/docs/content/docs/cua/reference/agent-sdk/index.mdx index ae458d5934..27a291ef95 100644 --- a/docs/content/docs/cua/reference/agent-sdk/index.mdx +++ b/docs/content/docs/cua/reference/agent-sdk/index.mdx @@ -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'; @@ -17,7 +17,7 @@ import { VersionHeader } from '@/components/version-selector'; @@ -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. diff --git a/docs/content/docs/cua/reference/cli/index.mdx b/docs/content/docs/cua/reference/cli/index.mdx index 134505206f..352721ac38 100644 --- a/docs/content/docs/cua/reference/cli/index.mdx +++ b/docs/content/docs/cua/reference/cli/index.mdx @@ -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'; @@ -17,7 +17,7 @@ import { VersionHeader } from '@/components/version-selector'; diff --git a/docs/content/docs/cua/reference/computer-sdk/index.mdx b/docs/content/docs/cua/reference/computer-sdk/index.mdx index cb1025a3c8..a7b64d911a 100644 --- a/docs/content/docs/cua/reference/computer-sdk/index.mdx +++ b/docs/content/docs/cua/reference/computer-sdk/index.mdx @@ -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'; @@ -17,7 +17,7 @@ import { VersionHeader } from '@/components/version-selector'; diff --git a/docs/content/docs/cuabench/reference/api.mdx b/docs/content/docs/cuabench/reference/api.mdx index 849c840192..a6ebc9abf3 100644 --- a/docs/content/docs/cuabench/reference/api.mdx +++ b/docs/content/docs/cuabench/reference/api.mdx @@ -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'; @@ -17,7 +17,7 @@ import { VersionHeader } from '@/components/version-selector'; @@ -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). diff --git a/libs/python/agent/agent/adapters/cua_adapter.py b/libs/python/agent/agent/adapters/cua_adapter.py index 7bfb4e0308..0301903064 100644 --- a/libs/python/agent/agent/adapters/cua_adapter.py +++ b/libs/python/agent/agent/adapters/cua_adapter.py @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/libs/python/computer-server/computer_server/main.py b/libs/python/computer-server/computer_server/main.py index 894d5b9453..28d405ecdc 100644 --- a/libs/python/computer-server/computer_server/main.py +++ b/libs/python/computer-server/computer_server/main.py @@ -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}", diff --git a/libs/python/computer/computer/interface/generic.py b/libs/python/computer/computer/interface/generic.py index ba56082414..ebecda0b19 100644 --- a/libs/python/computer/computer/interface/generic.py +++ b/libs/python/computer/computer/interface/generic.py @@ -5,6 +5,7 @@ import aiohttp import websockets +from core.http import cua_version_headers from PIL import Image from ..logger import Logger, LogLevel @@ -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: @@ -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: diff --git a/libs/python/computer/computer/providers/cloud/provider.py b/libs/python/computer/computer/providers/cloud/provider.py index 615fc46b50..aa66ebcee4 100644 --- a/libs/python/computer/computer/providers/cloud/provider.py +++ b/libs/python/computer/computer/providers/cloud/provider.py @@ -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") @@ -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 @@ -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: @@ -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: @@ -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): @@ -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): @@ -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): diff --git a/libs/python/computer/computer/providers/cloud/providerv2.py b/libs/python/computer/computer/providers/cloud/providerv2.py index d5519ac05c..707b6a4a31 100644 --- a/libs/python/computer/computer/providers/cloud/providerv2.py +++ b/libs/python/computer/computer/providers/cloud/providerv2.py @@ -18,6 +18,7 @@ import os import aiohttp +from core.http import cua_version_headers DEFAULT_API_BASE = os.getenv("CUA_API_BASE", "https://api.cua.ai") @@ -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 @@ -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: @@ -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: @@ -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): @@ -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): @@ -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): diff --git a/libs/python/core/core/__init__.py b/libs/python/core/core/__init__.py index 9716bfd2d2..1af2cfbe08 100644 --- a/libs/python/core/core/__init__.py +++ b/libs/python/core/core/__init__.py @@ -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 diff --git a/libs/python/core/core/http.py b/libs/python/core/core/http.py new file mode 100644 index 0000000000..b0e628db86 --- /dev/null +++ b/libs/python/core/core/http.py @@ -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} diff --git a/libs/python/cua-cli/cua_cli/api/client.py b/libs/python/cua-cli/cua_cli/api/client.py index 7dee69092c..5b92e9d479 100644 --- a/libs/python/cua-cli/cua_cli/api/client.py +++ b/libs/python/cua-cli/cua_cli/api/client.py @@ -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" @@ -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( diff --git a/libs/python/cua-cli/cua_cli/commands/auth.py b/libs/python/cua-cli/cua_cli/commands/auth.py index acb8b7de09..744ac27948 100644 --- a/libs/python/cua-cli/cua_cli/commands/auth.py +++ b/libs/python/cua-cli/cua_cli/commands/auth.py @@ -6,6 +6,7 @@ from typing import Any, Optional import aiohttp +from core.http import cua_version_headers from cua_cli.auth.browser import authenticate_via_browser from cua_cli.auth.store import clear_credentials, get_api_key, save_api_key from cua_cli.utils.async_utils import run_async @@ -160,7 +161,11 @@ def cmd_status(args: argparse.Namespace) -> int: async def _fetch(): url = f"{_get_api_base()}/v1/me" - headers = {"Authorization": f"Bearer {api_key}", "Accept": "application/json"} + headers = { + "Authorization": f"Bearer {api_key}", + "Accept": "application/json", + **cua_version_headers(), + } async with aiohttp.ClientSession() as session: timeout = aiohttp.ClientTimeout(total=10) async with session.get(url, headers=headers, timeout=timeout) as resp: diff --git a/libs/python/cua-cli/cua_cli/commands/do.py b/libs/python/cua-cli/cua_cli/commands/do.py index 6a2d80111c..2a68ba31cf 100644 --- a/libs/python/cua-cli/cua_cli/commands/do.py +++ b/libs/python/cua-cli/cua_cli/commands/do.py @@ -336,9 +336,10 @@ async def _send(provider_type: str, name: str, command: str, params: dict) -> di return await _host_dispatch(command, params) import aiohttp + from core.http import cua_version_headers api_url = await _get_api_url(provider_type, name) - headers = {"Content-Type": "application/json"} + headers = {"Content-Type": "application/json", **cua_version_headers()} if provider_type in ("cloud", "cloudv2"): from cua_cli.auth.store import get_api_key diff --git a/libs/python/cua-cli/cua_cli/commands/mcp.py b/libs/python/cua-cli/cua_cli/commands/mcp.py index 8e3ca017da..376b75fc4a 100644 --- a/libs/python/cua-cli/cua_cli/commands/mcp.py +++ b/libs/python/cua-cli/cua_cli/commands/mcp.py @@ -442,10 +442,13 @@ async def _send_command(sandbox_name: str, command: str, params: dict) -> dict: server_url = await _get_server_url(sandbox_name) api_key = get_api_key() + from core.http import cua_version_headers + headers = { "Content-Type": "application/json", "X-API-Key": api_key, "X-Container-Name": sandbox_name or default_sandbox, + **cua_version_headers(), } async with aiohttp.ClientSession() as session: diff --git a/libs/python/cua-cli/cua_cli/commands/sandbox.py b/libs/python/cua-cli/cua_cli/commands/sandbox.py index de5e7d276f..defbc711ba 100644 --- a/libs/python/cua-cli/cua_cli/commands/sandbox.py +++ b/libs/python/cua-cli/cua_cli/commands/sandbox.py @@ -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 from cua_cli.utils.async_utils import run_async from cua_cli.utils.output import ( @@ -37,6 +38,7 @@ async def _api_request( headers = { "Authorization": f"Bearer {api_key}", "Accept": "application/json", + **cua_version_headers(), } if json is not None: diff --git a/libs/typescript/agent/package.json b/libs/typescript/agent/package.json index 5d768a67bd..2cebcd76e5 100644 --- a/libs/typescript/agent/package.json +++ b/libs/typescript/agent/package.json @@ -38,7 +38,7 @@ "prepublishOnly": "pnpm run build" }, "dependencies": { - "@trycua/core": "^0.1.2", + "@trycua/core": "workspace:^", "peerjs": "^1.5.4", "pino": "^9.7.0" }, diff --git a/libs/typescript/agent/src/client.ts b/libs/typescript/agent/src/client.ts index 2ae4da6628..1e7216fe37 100644 --- a/libs/typescript/agent/src/client.ts +++ b/libs/typescript/agent/src/client.ts @@ -1,4 +1,5 @@ import { Peer } from 'peerjs'; +import { cuaVersionHeaders } from '@trycua/core'; import type { AgentRequest, AgentResponse, ConnectionType, AgentClientOptions } from './types'; export class AgentClient { @@ -52,6 +53,7 @@ export class AgentClient { try { const headers: Record = { 'Content-Type': 'application/json', + ...cuaVersionHeaders('agent', __CUA_VERSION__), }; if (this.options.apiKey) { headers['X-API-Key'] = this.options.apiKey; diff --git a/libs/typescript/agent/src/globals.d.ts b/libs/typescript/agent/src/globals.d.ts new file mode 100644 index 0000000000..f78c5ad001 --- /dev/null +++ b/libs/typescript/agent/src/globals.d.ts @@ -0,0 +1,2 @@ +/** Build-time constant injected by tsdown `define`. */ +declare const __CUA_VERSION__: string; diff --git a/libs/typescript/agent/tsdown.config.ts b/libs/typescript/agent/tsdown.config.ts index efbd5ff2ef..2953918b7c 100644 --- a/libs/typescript/agent/tsdown.config.ts +++ b/libs/typescript/agent/tsdown.config.ts @@ -1,5 +1,8 @@ +import { readFileSync } from 'node:fs'; import { defineConfig } from 'tsdown'; +const pkg = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf-8')); + export default defineConfig({ entry: ['src/index.ts'], format: ['module'], @@ -9,4 +12,7 @@ export default defineConfig({ // Remove if we don't need to support including the library via '