From 7cc51ddd623f71793c305dc83007e823403d2aa5 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 20:07:09 +0000 Subject: [PATCH] Add computer use provider base URLs Co-Authored-By: Shri --- .../client/managers/agents_validation.py | 22 +++++ .../agents/claude_computer_use.py | 10 +++ .../managers/async_manager/agents/cua.py | 6 ++ .../agents/gemini_computer_use.py | 6 ++ .../agents/claude_computer_use.py | 10 +++ .../managers/sync_manager/agents/cua.py | 6 ++ .../agents/gemini_computer_use.py | 6 ++ .../models/agents/claude_computer_use.py | 3 + hyperbrowser/models/agents/cua.py | 3 + .../models/agents/gemini_computer_use.py | 3 + tests/test_computer_use_wire_contract.py | 82 +++++++++++++++++++ 11 files changed, 157 insertions(+) create mode 100644 hyperbrowser/client/managers/agents_validation.py create mode 100644 tests/test_computer_use_wire_contract.py diff --git a/hyperbrowser/client/managers/agents_validation.py b/hyperbrowser/client/managers/agents_validation.py new file mode 100644 index 00000000..6640d1bc --- /dev/null +++ b/hyperbrowser/client/managers/agents_validation.py @@ -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") diff --git a/hyperbrowser/client/managers/async_manager/agents/claude_computer_use.py b/hyperbrowser/client/managers/async_manager/agents/claude_computer_use.py index e1461d0a..e1b5397f 100644 --- a/hyperbrowser/client/managers/async_manager/agents/claude_computer_use.py +++ b/hyperbrowser/client/managers/async_manager/agents/claude_computer_use.py @@ -2,6 +2,7 @@ from hyperbrowser.exceptions import HyperbrowserError +from ...agents_validation import validate_custom_api_keys from .....models import ( POLLING_ATTEMPTS, BasicResponse, @@ -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), diff --git a/hyperbrowser/client/managers/async_manager/agents/cua.py b/hyperbrowser/client/managers/async_manager/agents/cua.py index bce5a18b..3edfdf51 100644 --- a/hyperbrowser/client/managers/async_manager/agents/cua.py +++ b/hyperbrowser/client/managers/async_manager/agents/cua.py @@ -2,6 +2,7 @@ from hyperbrowser.exceptions import HyperbrowserError +from ...agents_validation import validate_custom_api_keys from .....models import ( POLLING_ATTEMPTS, BasicResponse, @@ -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), diff --git a/hyperbrowser/client/managers/async_manager/agents/gemini_computer_use.py b/hyperbrowser/client/managers/async_manager/agents/gemini_computer_use.py index 99e4a949..66e44bf5 100644 --- a/hyperbrowser/client/managers/async_manager/agents/gemini_computer_use.py +++ b/hyperbrowser/client/managers/async_manager/agents/gemini_computer_use.py @@ -2,6 +2,7 @@ from hyperbrowser.exceptions import HyperbrowserError +from ...agents_validation import validate_custom_api_keys from .....models import ( POLLING_ATTEMPTS, BasicResponse, @@ -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), diff --git a/hyperbrowser/client/managers/sync_manager/agents/claude_computer_use.py b/hyperbrowser/client/managers/sync_manager/agents/claude_computer_use.py index 81c34b1b..c1993d53 100644 --- a/hyperbrowser/client/managers/sync_manager/agents/claude_computer_use.py +++ b/hyperbrowser/client/managers/sync_manager/agents/claude_computer_use.py @@ -2,6 +2,7 @@ from hyperbrowser.exceptions import HyperbrowserError +from ...agents_validation import validate_custom_api_keys from .....models import ( POLLING_ATTEMPTS, BasicResponse, @@ -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), diff --git a/hyperbrowser/client/managers/sync_manager/agents/cua.py b/hyperbrowser/client/managers/sync_manager/agents/cua.py index d8b955c9..54f395da 100644 --- a/hyperbrowser/client/managers/sync_manager/agents/cua.py +++ b/hyperbrowser/client/managers/sync_manager/agents/cua.py @@ -2,6 +2,7 @@ from hyperbrowser.exceptions import HyperbrowserError +from ...agents_validation import validate_custom_api_keys from .....models import ( POLLING_ATTEMPTS, BasicResponse, @@ -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), diff --git a/hyperbrowser/client/managers/sync_manager/agents/gemini_computer_use.py b/hyperbrowser/client/managers/sync_manager/agents/gemini_computer_use.py index 766514a0..de2c1bc1 100644 --- a/hyperbrowser/client/managers/sync_manager/agents/gemini_computer_use.py +++ b/hyperbrowser/client/managers/sync_manager/agents/gemini_computer_use.py @@ -2,6 +2,7 @@ from hyperbrowser.exceptions import HyperbrowserError +from ...agents_validation import validate_custom_api_keys from .....models import ( POLLING_ATTEMPTS, BasicResponse, @@ -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), diff --git a/hyperbrowser/models/agents/claude_computer_use.py b/hyperbrowser/models/agents/claude_computer_use.py index 4200cce0..542d4841 100644 --- a/hyperbrowser/models/agents/claude_computer_use.py +++ b/hyperbrowser/models/agents/claude_computer_use.py @@ -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): diff --git a/hyperbrowser/models/agents/cua.py b/hyperbrowser/models/agents/cua.py index 6562e52f..f6cad6cf 100644 --- a/hyperbrowser/models/agents/cua.py +++ b/hyperbrowser/models/agents/cua.py @@ -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): diff --git a/hyperbrowser/models/agents/gemini_computer_use.py b/hyperbrowser/models/agents/gemini_computer_use.py index be7c6941..313fb8a6 100644 --- a/hyperbrowser/models/agents/gemini_computer_use.py +++ b/hyperbrowser/models/agents/gemini_computer_use.py @@ -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): diff --git a/tests/test_computer_use_wire_contract.py b/tests/test_computer_use_wire_contract.py new file mode 100644 index 00000000..a03448fb --- /dev/null +++ b/tests/test_computer_use_wire_contract.py @@ -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"})