Skip to content
Open
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
22 changes: 22 additions & 0 deletions hyperbrowser/client/managers/agents_validation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from typing import Mapping, Optional
from urllib.parse import urlparse

from hyperbrowser.exceptions import HyperbrowserError


def _is_absolute_http_url(value: str) -> bool:
parsed = urlparse(value)
return parsed.scheme in {"http", "https"} and bool(parsed.netloc)


def validate_custom_api_keys(
use_custom_api_keys: Optional[bool],
api_keys: Optional[object],
base_urls: Mapping[str, Optional[str]],
) -> None:
if use_custom_api_keys and api_keys is None:
raise HyperbrowserError("api_keys must be provided when use_custom_api_keys is true")

for field, value in base_urls.items():
if value is not None and not _is_absolute_http_url(value):
raise HyperbrowserError(f"{field} must be an absolute http or https URL")
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from hyperbrowser.exceptions import HyperbrowserError

from ...agents_validation import validate_custom_api_keys
from .....models import (
POLLING_ATTEMPTS,
BasicResponse,
Expand All @@ -19,6 +20,15 @@ def __init__(self, client):
async def start(
self, params: StartClaudeComputerUseTaskParams
) -> StartClaudeComputerUseTaskResponse:
validate_custom_api_keys(
params.use_custom_api_keys,
params.api_keys,
{
"anthropic_base_url": params.api_keys.anthropic_base_url
if params.api_keys
else None
},
)
response = await self._client.transport.post(
self._client._build_url("/task/claude-computer-use"),
data=params.model_dump(exclude_none=True, by_alias=True),
Expand Down
6 changes: 6 additions & 0 deletions hyperbrowser/client/managers/async_manager/agents/cua.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from hyperbrowser.exceptions import HyperbrowserError

from ...agents_validation import validate_custom_api_keys
from .....models import (
POLLING_ATTEMPTS,
BasicResponse,
Expand All @@ -17,6 +18,11 @@ def __init__(self, client):
self._client = client

async def start(self, params: StartCuaTaskParams) -> StartCuaTaskResponse:
validate_custom_api_keys(
params.use_custom_api_keys,
params.api_keys,
{"openai_base_url": params.api_keys.openai_base_url if params.api_keys else None},
)
response = await self._client.transport.post(
self._client._build_url("/task/cua"),
data=params.model_dump(exclude_none=True, by_alias=True),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from hyperbrowser.exceptions import HyperbrowserError

from ...agents_validation import validate_custom_api_keys
from .....models import (
POLLING_ATTEMPTS,
BasicResponse,
Expand All @@ -19,6 +20,11 @@ def __init__(self, client):
async def start(
self, params: StartGeminiComputerUseTaskParams
) -> StartGeminiComputerUseTaskResponse:
validate_custom_api_keys(
params.use_custom_api_keys,
params.api_keys,
{"google_base_url": params.api_keys.google_base_url if params.api_keys else None},
)
response = await self._client.transport.post(
self._client._build_url("/task/gemini-computer-use"),
data=params.model_dump(exclude_none=True, by_alias=True),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from hyperbrowser.exceptions import HyperbrowserError

from ...agents_validation import validate_custom_api_keys
from .....models import (
POLLING_ATTEMPTS,
BasicResponse,
Expand All @@ -19,6 +20,15 @@ def __init__(self, client):
def start(
self, params: StartClaudeComputerUseTaskParams
) -> StartClaudeComputerUseTaskResponse:
validate_custom_api_keys(
params.use_custom_api_keys,
params.api_keys,
{
"anthropic_base_url": params.api_keys.anthropic_base_url
if params.api_keys
else None
},
)
response = self._client.transport.post(
self._client._build_url("/task/claude-computer-use"),
data=params.model_dump(exclude_none=True, by_alias=True),
Expand Down
6 changes: 6 additions & 0 deletions hyperbrowser/client/managers/sync_manager/agents/cua.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from hyperbrowser.exceptions import HyperbrowserError

from ...agents_validation import validate_custom_api_keys
from .....models import (
POLLING_ATTEMPTS,
BasicResponse,
Expand All @@ -17,6 +18,11 @@ def __init__(self, client):
self._client = client

def start(self, params: StartCuaTaskParams) -> StartCuaTaskResponse:
validate_custom_api_keys(
params.use_custom_api_keys,
params.api_keys,
{"openai_base_url": params.api_keys.openai_base_url if params.api_keys else None},
)
response = self._client.transport.post(
self._client._build_url("/task/cua"),
data=params.model_dump(exclude_none=True, by_alias=True),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from hyperbrowser.exceptions import HyperbrowserError

from ...agents_validation import validate_custom_api_keys
from .....models import (
POLLING_ATTEMPTS,
BasicResponse,
Expand All @@ -19,6 +20,11 @@ def __init__(self, client):
def start(
self, params: StartGeminiComputerUseTaskParams
) -> StartGeminiComputerUseTaskResponse:
validate_custom_api_keys(
params.use_custom_api_keys,
params.api_keys,
{"google_base_url": params.api_keys.google_base_url if params.api_keys else None},
)
response = self._client.transport.post(
self._client._build_url("/task/gemini-computer-use"),
data=params.model_dump(exclude_none=True, by_alias=True),
Expand Down
3 changes: 3 additions & 0 deletions hyperbrowser/models/agents/claude_computer_use.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ class ClaudeComputerUseApiKeys(BaseModel):
)

anthropic: Optional[str] = Field(default=None, serialization_alias="anthropic")
anthropic_base_url: Optional[str] = Field(
default=None, serialization_alias="anthropicBaseUrl"
)


class StartClaudeComputerUseTaskParams(BaseModel):
Expand Down
3 changes: 3 additions & 0 deletions hyperbrowser/models/agents/cua.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ class CuaApiKeys(BaseModel):
)

openai: Optional[str] = Field(default=None, serialization_alias="openai")
openai_base_url: Optional[str] = Field(
default=None, serialization_alias="openaiBaseUrl"
)


class StartCuaTaskParams(BaseModel):
Expand Down
3 changes: 3 additions & 0 deletions hyperbrowser/models/agents/gemini_computer_use.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ class GeminiComputerUseApiKeys(BaseModel):
)

google: Optional[str] = Field(default=None, serialization_alias="google")
google_base_url: Optional[str] = Field(
default=None, serialization_alias="googleBaseUrl"
)


class StartGeminiComputerUseTaskParams(BaseModel):
Expand Down
82 changes: 82 additions & 0 deletions tests/test_computer_use_wire_contract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import pytest

from hyperbrowser.client.managers.agents_validation import validate_custom_api_keys
from hyperbrowser.exceptions import HyperbrowserError
from hyperbrowser.models import (
ClaudeComputerUseApiKeys,
CuaApiKeys,
GeminiComputerUseApiKeys,
StartClaudeComputerUseTaskParams,
StartCuaTaskParams,
StartGeminiComputerUseTaskParams,
)


def test_cua_api_keys_serialize_openai_base_url() -> None:
params = StartCuaTaskParams(
task="go to example.com",
use_custom_api_keys=True,
api_keys=CuaApiKeys(
openai="sk-test",
openai_base_url="https://openai-compatible.example.com/v1",
),
)

assert params.model_dump(exclude_none=True, by_alias=True) == {
"task": "go to example.com",
"useCustomApiKeys": True,
"apiKeys": {
"openai": "sk-test",
"openaiBaseUrl": "https://openai-compatible.example.com/v1",
},
}


def test_claude_computer_use_api_keys_serialize_anthropic_base_url() -> None:
params = StartClaudeComputerUseTaskParams(
task="go to example.com",
use_custom_api_keys=True,
api_keys=ClaudeComputerUseApiKeys(
anthropic="sk-ant-test",
anthropic_base_url="https://anthropic-compatible.example.com",
),
)

assert params.model_dump(exclude_none=True, by_alias=True) == {
"task": "go to example.com",
"useCustomApiKeys": True,
"apiKeys": {
"anthropic": "sk-ant-test",
"anthropicBaseUrl": "https://anthropic-compatible.example.com",
},
}


def test_gemini_computer_use_api_keys_serialize_google_base_url() -> None:
params = StartGeminiComputerUseTaskParams(
task="go to example.com",
use_custom_api_keys=True,
api_keys=GeminiComputerUseApiKeys(
google="google-test",
google_base_url="https://gemini-compatible.example.com",
),
)

assert params.model_dump(exclude_none=True, by_alias=True) == {
"task": "go to example.com",
"useCustomApiKeys": True,
"apiKeys": {
"google": "google-test",
"googleBaseUrl": "https://gemini-compatible.example.com",
},
}


def test_custom_api_key_mode_requires_api_keys() -> None:
with pytest.raises(HyperbrowserError, match="api_keys must be provided"):
validate_custom_api_keys(True, None, {})


def test_provider_base_urls_must_be_absolute_http_urls() -> None:
with pytest.raises(HyperbrowserError, match="openai_base_url must be an absolute"):
validate_custom_api_keys(False, None, {"openai_base_url": "localhost:3000/v1"})