From 90a8e7984e6855b721bd1dab6d3f057e187ecbcf Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 22 Sep 2026 19:47:03 +0000 Subject: [PATCH 1/5] Add Jev Computer Use, Claude Opus 5.5, and GPT-6 Sol/Luna Add the Jev Computer Use manager and accept the latest Claude and OpenAI CUA models from browserctrl. Co-authored-by: nikhil --- CLAUDE.md | 2 +- .../managers/async_manager/agents/__init__.py | 2 + .../async_manager/agents/jev_computer_use.py | 84 +++++++++ .../managers/sync_manager/agents/__init__.py | 2 + .../sync_manager/agents/jev_computer_use.py | 84 +++++++++ hyperbrowser/models/__init__.py | 24 +++ .../models/agents/jev_computer_use.py | 168 ++++++++++++++++++ hyperbrowser/models/consts.py | 8 + hyperbrowser/types/__init__.py | 4 + hyperbrowser/types/agents.py | 28 +++ pyproject.toml | 2 +- tests/test_claude_computer_use_models.py | 17 ++ tests/test_cua_models.py | 20 +++ tests/test_jev_computer_use_models.py | 77 ++++++++ tests/test_request_compat.py | 43 ++++- tests/typecheck/valid_requests.py | 23 ++- 16 files changed, 580 insertions(+), 8 deletions(-) create mode 100644 hyperbrowser/client/managers/async_manager/agents/jev_computer_use.py create mode 100644 hyperbrowser/client/managers/sync_manager/agents/jev_computer_use.py create mode 100644 hyperbrowser/models/agents/jev_computer_use.py create mode 100644 tests/test_claude_computer_use_models.py create mode 100644 tests/test_cua_models.py create mode 100644 tests/test_jev_computer_use_models.py diff --git a/CLAUDE.md b/CLAUDE.md index 1a8af886..2889e373 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -47,7 +47,7 @@ client.crawl # Website crawling jobs client.extract # Data extraction jobs client.web # Web fetch/search operations client.web.batch_fetch # Batch web fetch operations -client.agents # AI agent integrations (browser_use, cua, claude_computer_use, hyper_agent, gemini_computer_use, grok_computer_use, meta_computer_use) +client.agents # AI agent integrations (browser_use, cua, claude_computer_use, hyper_agent, gemini_computer_use, grok_computer_use, jev_computer_use, meta_computer_use) client.profiles # Browser profile management client.extensions # Browser extension management client.team # Team/credit info diff --git a/hyperbrowser/client/managers/async_manager/agents/__init__.py b/hyperbrowser/client/managers/async_manager/agents/__init__.py index 601993bd..150101f5 100644 --- a/hyperbrowser/client/managers/async_manager/agents/__init__.py +++ b/hyperbrowser/client/managers/async_manager/agents/__init__.py @@ -4,6 +4,7 @@ from .hyper_agent import HyperAgentManager from .gemini_computer_use import GeminiComputerUseManager from .grok_computer_use import GrokComputerUseManager +from .jev_computer_use import JevComputerUseManager from .meta_computer_use import MetaComputerUseManager @@ -15,4 +16,5 @@ def __init__(self, client): self.hyper_agent = HyperAgentManager(client) self.gemini_computer_use = GeminiComputerUseManager(client) self.grok_computer_use = GrokComputerUseManager(client) + self.jev_computer_use = JevComputerUseManager(client) self.meta_computer_use = MetaComputerUseManager(client) diff --git a/hyperbrowser/client/managers/async_manager/agents/jev_computer_use.py b/hyperbrowser/client/managers/async_manager/agents/jev_computer_use.py new file mode 100644 index 00000000..21f9c456 --- /dev/null +++ b/hyperbrowser/client/managers/async_manager/agents/jev_computer_use.py @@ -0,0 +1,84 @@ +import asyncio +from typing import Union + +from hyperbrowser.client._request import dump_request +from hyperbrowser.exceptions import HyperbrowserError +from hyperbrowser.types import ( + StartJevComputerUseTaskParams as StartJevComputerUseTaskParamsDict, +) + +from .....models import ( + POLLING_ATTEMPTS, + BasicResponse, + JevComputerUseTaskResponse, + JevComputerUseTaskStatusResponse, + StartJevComputerUseTaskParams, + StartJevComputerUseTaskResponse, +) + + +class JevComputerUseManager: + def __init__(self, client): + self._client = client + + async def start( + self, + params: Union[ + StartJevComputerUseTaskParamsDict, + StartJevComputerUseTaskParams, + ], + ) -> StartJevComputerUseTaskResponse: + response = await self._client.transport.post( + self._client._build_url("/task/jev"), + data=dump_request(params, StartJevComputerUseTaskParams), + ) + return StartJevComputerUseTaskResponse(**response.data) + + async def get(self, job_id: str) -> JevComputerUseTaskResponse: + response = await self._client.transport.get( + self._client._build_url(f"/task/jev/{job_id}") + ) + return JevComputerUseTaskResponse(**response.data) + + async def get_status(self, job_id: str) -> JevComputerUseTaskStatusResponse: + response = await self._client.transport.get( + self._client._build_url(f"/task/jev/{job_id}/status") + ) + return JevComputerUseTaskStatusResponse(**response.data) + + async def stop(self, job_id: str) -> BasicResponse: + response = await self._client.transport.put( + self._client._build_url(f"/task/jev/{job_id}/stop") + ) + return BasicResponse(**response.data) + + async def start_and_wait( + self, + params: Union[ + StartJevComputerUseTaskParamsDict, + StartJevComputerUseTaskParams, + ], + ) -> JevComputerUseTaskResponse: + job_start_resp = await self.start(params) + job_id = job_start_resp.job_id + if not job_id: + raise HyperbrowserError("Failed to start Jev Computer Use task job") + + failures = 0 + while True: + try: + job_response = await self.get_status(job_id) + if ( + job_response.status == "completed" + or job_response.status == "failed" + or job_response.status == "stopped" + ): + return await self.get(job_id) + failures = 0 + except Exception as e: + failures += 1 + if failures >= POLLING_ATTEMPTS: + raise HyperbrowserError( + f"Failed to poll Jev Computer Use task job {job_id} after {POLLING_ATTEMPTS} attempts: {e}" + ) + await asyncio.sleep(2) diff --git a/hyperbrowser/client/managers/sync_manager/agents/__init__.py b/hyperbrowser/client/managers/sync_manager/agents/__init__.py index 601993bd..150101f5 100644 --- a/hyperbrowser/client/managers/sync_manager/agents/__init__.py +++ b/hyperbrowser/client/managers/sync_manager/agents/__init__.py @@ -4,6 +4,7 @@ from .hyper_agent import HyperAgentManager from .gemini_computer_use import GeminiComputerUseManager from .grok_computer_use import GrokComputerUseManager +from .jev_computer_use import JevComputerUseManager from .meta_computer_use import MetaComputerUseManager @@ -15,4 +16,5 @@ def __init__(self, client): self.hyper_agent = HyperAgentManager(client) self.gemini_computer_use = GeminiComputerUseManager(client) self.grok_computer_use = GrokComputerUseManager(client) + self.jev_computer_use = JevComputerUseManager(client) self.meta_computer_use = MetaComputerUseManager(client) diff --git a/hyperbrowser/client/managers/sync_manager/agents/jev_computer_use.py b/hyperbrowser/client/managers/sync_manager/agents/jev_computer_use.py new file mode 100644 index 00000000..f0c5e196 --- /dev/null +++ b/hyperbrowser/client/managers/sync_manager/agents/jev_computer_use.py @@ -0,0 +1,84 @@ +import time +from typing import Union + +from hyperbrowser.client._request import dump_request +from hyperbrowser.exceptions import HyperbrowserError +from hyperbrowser.types import ( + StartJevComputerUseTaskParams as StartJevComputerUseTaskParamsDict, +) + +from .....models import ( + POLLING_ATTEMPTS, + BasicResponse, + JevComputerUseTaskResponse, + JevComputerUseTaskStatusResponse, + StartJevComputerUseTaskParams, + StartJevComputerUseTaskResponse, +) + + +class JevComputerUseManager: + def __init__(self, client): + self._client = client + + def start( + self, + params: Union[ + StartJevComputerUseTaskParamsDict, + StartJevComputerUseTaskParams, + ], + ) -> StartJevComputerUseTaskResponse: + response = self._client.transport.post( + self._client._build_url("/task/jev"), + data=dump_request(params, StartJevComputerUseTaskParams), + ) + return StartJevComputerUseTaskResponse(**response.data) + + def get(self, job_id: str) -> JevComputerUseTaskResponse: + response = self._client.transport.get( + self._client._build_url(f"/task/jev/{job_id}") + ) + return JevComputerUseTaskResponse(**response.data) + + def get_status(self, job_id: str) -> JevComputerUseTaskStatusResponse: + response = self._client.transport.get( + self._client._build_url(f"/task/jev/{job_id}/status") + ) + return JevComputerUseTaskStatusResponse(**response.data) + + def stop(self, job_id: str) -> BasicResponse: + response = self._client.transport.put( + self._client._build_url(f"/task/jev/{job_id}/stop") + ) + return BasicResponse(**response.data) + + def start_and_wait( + self, + params: Union[ + StartJevComputerUseTaskParamsDict, + StartJevComputerUseTaskParams, + ], + ) -> JevComputerUseTaskResponse: + job_start_resp = self.start(params) + job_id = job_start_resp.job_id + if not job_id: + raise HyperbrowserError("Failed to start Jev Computer Use task job") + + failures = 0 + while True: + try: + job_response = self.get_status(job_id) + if ( + job_response.status == "completed" + or job_response.status == "failed" + or job_response.status == "stopped" + ): + return self.get(job_id) + failures = 0 + except Exception as e: + failures += 1 + if failures >= POLLING_ATTEMPTS: + raise HyperbrowserError( + f"Failed to poll Jev Computer Use task job {job_id} after {POLLING_ATTEMPTS} attempts: {e}" + ) + time.sleep(2) diff --git a/hyperbrowser/models/__init__.py b/hyperbrowser/models/__init__.py index 49d8f29e..3aae0e05 100644 --- a/hyperbrowser/models/__init__.py +++ b/hyperbrowser/models/__init__.py @@ -138,6 +138,17 @@ StartMetaComputerUseTaskResponse, MetaComputerUseApiKeys, ) +from .agents.jev_computer_use import ( + JevComputerUseTaskStatus, + JevComputerUseAction, + JevComputerUseStepResponse, + JevComputerUseTaskData, + JevComputerUseTaskResponse, + JevComputerUseTaskStatusResponse, + StartJevComputerUseTaskParams, + StartJevComputerUseTaskResponse, + JevComputerUseApiKeys, +) from .agents.cua import ( CuaTaskData, CuaTaskResponse, @@ -175,6 +186,8 @@ GrokReasoningEffort, MetaComputerUseLlm, MetaReasoningEffort, + JevComputerUseLlm, + JevTextLlm, Country, DownloadsStatus, FetchScreenshotFormat, @@ -521,6 +534,14 @@ "MetaComputerUseTaskStatusResponse", "StartMetaComputerUseTaskParams", "StartMetaComputerUseTaskResponse", + "JevComputerUseTaskStatus", + "JevComputerUseAction", + "JevComputerUseStepResponse", + "JevComputerUseTaskData", + "JevComputerUseTaskResponse", + "JevComputerUseTaskStatusResponse", + "StartJevComputerUseTaskParams", + "StartJevComputerUseTaskResponse", "CuaTaskStatus", "CuaTaskData", "CuaTaskResponse", @@ -531,6 +552,7 @@ "ClaudeComputerUseApiKeys", "GeminiComputerUseApiKeys", "GrokComputerUseApiKeys", + "JevComputerUseApiKeys", "MetaComputerUseApiKeys", "CuaApiKeys", "CuaBaseUrls", @@ -539,6 +561,8 @@ "GrokReasoningEffort", "MetaComputerUseLlm", "MetaReasoningEffort", + "JevComputerUseLlm", + "JevTextLlm", "HyperAgentOutputV110", "HyperAgentStepV110", # crawl diff --git a/hyperbrowser/models/agents/jev_computer_use.py b/hyperbrowser/models/agents/jev_computer_use.py new file mode 100644 index 00000000..fd774efd --- /dev/null +++ b/hyperbrowser/models/agents/jev_computer_use.py @@ -0,0 +1,168 @@ +from __future__ import annotations + +from typing import List, Literal, Optional + +from pydantic import BaseModel, ConfigDict, Field + +from ..session import CreateSessionParams +from ..consts import JevComputerUseLlm, JevTextLlm + +JevComputerUseTaskStatus = Literal[ + "pending", "running", "completed", "failed", "stopped" +] + + +class JevComputerUseApiKeys(BaseModel): + """ + API keys for the Jev Computer Use task. + """ + + model_config = ConfigDict( + populate_by_alias=True, + ) + + jev: Optional[str] = Field(default=None, serialization_alias="jev") + google: Optional[str] = Field(default=None, serialization_alias="google") + + +class StartJevComputerUseTaskParams(BaseModel): + """ + Parameters for creating a new Jev Computer Use task. + """ + + model_config = ConfigDict( + populate_by_alias=True, + ) + + task: str + llm: Optional[JevComputerUseLlm] = Field(default=None, serialization_alias="llm") + text_llm: Optional[JevTextLlm] = Field(default=None, serialization_alias="textLlm") + start_url: Optional[str] = Field(default=None, serialization_alias="startUrl") + session_id: Optional[str] = Field(default=None, serialization_alias="sessionId") + max_failures: Optional[int] = Field(default=None, serialization_alias="maxFailures") + max_steps: Optional[int] = Field(default=None, serialization_alias="maxSteps") + keep_browser_open: Optional[bool] = Field( + default=None, serialization_alias="keepBrowserOpen" + ) + session_options: Optional[CreateSessionParams] = Field( + default=None, serialization_alias="sessionOptions" + ) + use_custom_api_keys: Optional[bool] = Field( + default=None, serialization_alias="useCustomApiKeys" + ) + api_keys: Optional[JevComputerUseApiKeys] = Field( + default=None, serialization_alias="apiKeys" + ) + use_computer_action: Optional[bool] = Field( + default=None, serialization_alias="useComputerAction" + ) + + +class StartJevComputerUseTaskResponse(BaseModel): + """ + Response from starting a Jev Computer Use task. + """ + + model_config = ConfigDict( + populate_by_alias=True, + ) + + job_id: str = Field(alias="jobId") + live_url: Optional[str] = Field(default=None, alias="liveUrl") + + +class JevComputerUseTaskStatusResponse(BaseModel): + """ + Response from getting a Jev Computer Use task status. + """ + + model_config = ConfigDict( + populate_by_alias=True, + ) + + status: JevComputerUseTaskStatus + + +class JevComputerUseAction(BaseModel): + """ + A single Jev action recorded during a task step. + """ + + model_config = ConfigDict( + populate_by_alias=True, + ) + + kind: str + target: Optional[str] = None + option: Optional[str] = None + + +class JevComputerUseStepResponse(BaseModel): + """ + Response from a single Jev Computer Use step. + """ + + model_config = ConfigDict( + populate_by_alias=True, + ) + + step: int + action: JevComputerUseAction + url: str + value: Optional[str] = None + title: Optional[str] = None + page_text: Optional[str] = Field(default=None, alias="pageText") + target_description: Optional[str] = Field(default=None, alias="targetDescription") + confidence: Optional[float] = None + error: Optional[str] = None + error_kind: Optional[str] = Field(default=None, alias="errorKind") + page_changed: Optional[bool] = Field(default=None, alias="pageChanged") + outcome: Optional[str] = None + after: Optional[str] = None + before_tab: Optional[str] = Field(default=None, alias="beforeTab") + after_tab: Optional[str] = Field(default=None, alias="afterTab") + before_observation: Optional[int] = Field(default=None, alias="beforeObservation") + after_observation: Optional[int] = Field(default=None, alias="afterObservation") + + +class JevComputerUseTaskData(BaseModel): + model_config = ConfigDict( + populate_by_alias=True, + ) + + steps: List[JevComputerUseStepResponse] + final_result: Optional[str] = Field(default=None, alias="finalResult") + reached_max_steps: Optional[bool] = Field(default=None, alias="reachedMaxSteps") + final_url: Optional[str] = Field(default=None, alias="finalUrl") + warnings: Optional[List[str]] = None + + +class JevComputerUseTaskMetadata(BaseModel): + model_config = ConfigDict( + populate_by_alias=True, + ) + + input_tokens: Optional[int] = Field(default=None, alias="inputTokens") + output_tokens: Optional[int] = Field(default=None, alias="outputTokens") + num_task_steps_completed: Optional[int] = Field( + default=None, alias="numTaskStepsCompleted" + ) + + +class JevComputerUseTaskResponse(BaseModel): + """ + Response from a Jev Computer Use task. + """ + + model_config = ConfigDict( + populate_by_alias=True, + ) + + job_id: str = Field(alias="jobId") + status: JevComputerUseTaskStatus + metadata: Optional[JevComputerUseTaskMetadata] = Field( + default=None, alias="metadata" + ) + data: Optional[JevComputerUseTaskData] = Field(default=None, alias="data") + error: Optional[str] = Field(default=None, alias="error") + live_url: Optional[str] = Field(default=None, alias="liveUrl") diff --git a/hyperbrowser/models/consts.py b/hyperbrowser/models/consts.py index 698e8a3f..ec3de57c 100644 --- a/hyperbrowser/models/consts.py +++ b/hyperbrowser/models/consts.py @@ -68,6 +68,7 @@ "gemini-3-flash-preview", ] ClaudeComputerUseLlm = Literal[ + "claude-opus-5-5", "claude-fable-5-1", "claude-opus-5", "claude-opus-4-5", @@ -91,6 +92,8 @@ CuaLlm = Literal[ "computer-use-preview", "gpt-6-astra", + "gpt-6-sol", + "gpt-6-luna", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", @@ -138,6 +141,11 @@ "xhigh", "max", ] +JevComputerUseLlm = Literal[ + "jev-1.13.0", + "jev-latest", +] +JevTextLlm = Literal["gemini-3.5-flash-lite",] SessionRegion = Literal[ "us", "us-central", diff --git a/hyperbrowser/types/__init__.py b/hyperbrowser/types/__init__.py index f0ebdfc8..4633a631 100644 --- a/hyperbrowser/types/__init__.py +++ b/hyperbrowser/types/__init__.py @@ -20,6 +20,7 @@ GeminiComputerUseApiKeys, GrokComputerUseApiKeys, HyperAgentApiKeys, + JevComputerUseApiKeys, MetaComputerUseApiKeys, StartBrowserUseTaskParams, StartClaudeComputerUseTaskParams, @@ -27,6 +28,7 @@ StartGeminiComputerUseTaskParams, StartGrokComputerUseTaskParams, StartHyperAgentTaskParams, + StartJevComputerUseTaskParams, StartMetaComputerUseTaskParams, ) from .computer_action import ( @@ -186,6 +188,7 @@ "GeminiComputerUseApiKeys", "GrokComputerUseApiKeys", "HyperAgentApiKeys", + "JevComputerUseApiKeys", "MetaComputerUseApiKeys", "StartBrowserUseTaskParams", "StartClaudeComputerUseTaskParams", @@ -193,6 +196,7 @@ "StartGeminiComputerUseTaskParams", "StartGrokComputerUseTaskParams", "StartHyperAgentTaskParams", + "StartJevComputerUseTaskParams", "StartMetaComputerUseTaskParams", "ClickActionParams", "ComputerActionMouseButton", diff --git a/hyperbrowser/types/agents.py b/hyperbrowser/types/agents.py index dc8b4b8a..edc748a0 100644 --- a/hyperbrowser/types/agents.py +++ b/hyperbrowser/types/agents.py @@ -14,6 +14,8 @@ GrokReasoningEffort, MetaComputerUseLlm, MetaReasoningEffort, + JevComputerUseLlm, + JevTextLlm, HyperAgentLlm, HyperAgentVersion, ) @@ -144,6 +146,30 @@ class StartMetaComputerUseTaskParams(TypedDict, total=False): use_computer_action: Optional[bool] +class JevComputerUseApiKeys(TypedDict, total=False): + """Provider API keys for a Jev Computer Use task.""" + + jev: Optional[str] + google: Optional[str] + + +class StartJevComputerUseTaskParams(TypedDict, total=False): + """Parameters for starting a Jev Computer Use task.""" + + task: Required[str] + llm: Optional[JevComputerUseLlm] + text_llm: Optional[JevTextLlm] + start_url: Optional[str] + session_id: Optional[str] + max_failures: Optional[int] + max_steps: Optional[int] + keep_browser_open: Optional[bool] + session_options: Optional[CreateSessionParams] + use_custom_api_keys: Optional[bool] + api_keys: Optional[JevComputerUseApiKeys] + use_computer_action: Optional[bool] + + class CuaApiKeys(TypedDict, total=False): """Provider API keys for an OpenAI CUA task.""" @@ -204,6 +230,7 @@ class StartHyperAgentTaskParams(TypedDict, total=False): "GeminiComputerUseApiKeys", "GrokComputerUseApiKeys", "HyperAgentApiKeys", + "JevComputerUseApiKeys", "MetaComputerUseApiKeys", "StartBrowserUseTaskParams", "StartClaudeComputerUseTaskParams", @@ -211,5 +238,6 @@ class StartHyperAgentTaskParams(TypedDict, total=False): "StartGeminiComputerUseTaskParams", "StartGrokComputerUseTaskParams", "StartHyperAgentTaskParams", + "StartJevComputerUseTaskParams", "StartMetaComputerUseTaskParams", ] diff --git a/pyproject.toml b/pyproject.toml index 1fe69998..eb223a03 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "hyperbrowser" -version = "1.5.0" +version = "1.6.0" description = "Python SDK for hyperbrowser" authors = ["Nikhil Shahi "] license = "MIT" diff --git a/tests/test_claude_computer_use_models.py b/tests/test_claude_computer_use_models.py new file mode 100644 index 00000000..18c3b930 --- /dev/null +++ b/tests/test_claude_computer_use_models.py @@ -0,0 +1,17 @@ +from hyperbrowser.client._request import dump_request +from hyperbrowser.models.agents.claude_computer_use import ( + StartClaudeComputerUseTaskParams, +) + + +def test_start_claude_computer_use_accepts_opus_5_5() -> None: + dumped = dump_request( + { + "task": "Inspect the page", + "llm": "claude-opus-5-5", + "reasoning_effort": "max", + }, + StartClaudeComputerUseTaskParams, + ) + assert dumped["llm"] == "claude-opus-5-5" + assert dumped["reasoningEffort"] == "max" diff --git a/tests/test_cua_models.py b/tests/test_cua_models.py new file mode 100644 index 00000000..c196ba1e --- /dev/null +++ b/tests/test_cua_models.py @@ -0,0 +1,20 @@ +import pytest +from pydantic import ValidationError + +from hyperbrowser.client._request import dump_request +from hyperbrowser.models.agents.cua import StartCuaTaskParams + + +@pytest.mark.parametrize("llm", ["gpt-6-sol", "gpt-6-luna", "gpt-6-astra"]) +def test_start_cua_accepts_gpt6_models(llm: str) -> None: + dumped = dump_request( + {"task": "Inspect the page", "llm": llm, "reasoning_effort": "max"}, + StartCuaTaskParams, + ) + assert dumped["llm"] == llm + assert dumped["reasoningEffort"] == "max" + + +def test_start_cua_rejects_unknown_model() -> None: + with pytest.raises(ValidationError): + StartCuaTaskParams(task="Inspect the page", llm="gpt-6-orbit") diff --git a/tests/test_jev_computer_use_models.py b/tests/test_jev_computer_use_models.py new file mode 100644 index 00000000..54b771e7 --- /dev/null +++ b/tests/test_jev_computer_use_models.py @@ -0,0 +1,77 @@ +import pytest +from pydantic import ValidationError + +from hyperbrowser.client._request import dump_request +from hyperbrowser.models.agents.jev_computer_use import ( + JevComputerUseTaskResponse, + StartJevComputerUseTaskParams, +) + + +def test_jev_computer_use_task_response_parses_step_payload() -> None: + payload = { + "jobId": "job-123", + "status": "completed", + "data": { + "steps": [ + { + "step": 1, + "action": {"kind": "click", "target": "submit"}, + "url": "https://example.com", + "title": "Example", + "pageText": "Order details", + "targetDescription": "Submit button", + "confidence": 0.91, + "outcome": "dispatched", + "pageChanged": True, + "beforeObservation": 0, + "afterObservation": 1, + } + ], + "finalResult": "Found the order", + "reachedMaxSteps": False, + "finalUrl": "https://example.com/orders/1", + "warnings": [], + }, + } + + result = JevComputerUseTaskResponse(**payload) + + step = result.data.steps[0] + assert step.step == 1 + assert step.action.kind == "click" + assert step.action.target == "submit" + assert step.page_text == "Order details" + assert step.target_description == "Submit button" + assert step.page_changed is True + assert result.data.final_result == "Found the order" + assert result.data.final_url == "https://example.com/orders/1" + assert result.data.reached_max_steps is False + + +@pytest.mark.parametrize("llm", ["jev-1.13.0", "jev-latest"]) +def test_start_jev_computer_use_accepts_api_models(llm: str) -> None: + model_params = StartJevComputerUseTaskParams( + task="Find the order", + llm=llm, + text_llm="gemini-3.5-flash-lite", + start_url="https://example.com", + ) + dict_params = { + "task": "Find the order", + "llm": llm, + "text_llm": "gemini-3.5-flash-lite", + "start_url": "https://example.com", + } + + for params in (model_params, dict_params): + dumped = dump_request(params, StartJevComputerUseTaskParams) + assert dumped["task"] == "Find the order" + assert dumped["llm"] == llm + assert dumped["textLlm"] == "gemini-3.5-flash-lite" + assert dumped["startUrl"] == "https://example.com" + + +def test_start_jev_computer_use_rejects_unknown_model() -> None: + with pytest.raises(ValidationError): + StartJevComputerUseTaskParams(task="Find the order", llm="jev-1.0.0") diff --git a/tests/test_request_compat.py b/tests/test_request_compat.py index 4fe23f5b..188480be 100644 --- a/tests/test_request_compat.py +++ b/tests/test_request_compat.py @@ -12,10 +12,12 @@ CuaBaseUrls, CreateSandboxParams, CreateSessionParams, + JevComputerUseApiKeys, MetaComputerUseApiKeys, StartBrowserUseTaskParams, StartClaudeComputerUseTaskParams, StartCuaTaskParams, + StartJevComputerUseTaskParams, StartMetaComputerUseTaskParams, ) from hyperbrowser.tools import _normalize_extract_tool_params @@ -121,14 +123,14 @@ def test_cua_custom_base_url_serializes_for_mapping_and_legacy_model(): def test_claude_computer_use_serializes_reasoning_effort_for_mapping_and_legacy_model(): mapping = { "task": "Complete the task", - "llm": "claude-opus-5", + "llm": "claude-opus-5-5", "reasoning_effort": "xhigh", "use_custom_api_keys": True, "api_keys": {"anthropic": "anthropic-key"}, } legacy = StartClaudeComputerUseTaskParams( task="Complete the task", - llm="claude-opus-5", + llm="claude-opus-5-5", reasoning_effort="xhigh", use_custom_api_keys=True, api_keys=ClaudeComputerUseApiKeys(anthropic="anthropic-key"), @@ -140,7 +142,7 @@ def test_claude_computer_use_serializes_reasoning_effort_for_mapping_and_legacy_ ) assert dump_request(mapping, StartClaudeComputerUseTaskParams) == { "task": "Complete the task", - "llm": "claude-opus-5", + "llm": "claude-opus-5-5", "reasoningEffort": "xhigh", "useCustomApiKeys": True, "apiKeys": {"anthropic": "anthropic-key"}, @@ -179,6 +181,41 @@ def test_meta_computer_use_serializes_for_mapping_and_legacy_model(): } +def test_jev_computer_use_serializes_for_mapping_and_legacy_model(): + mapping = { + "task": "Find the order", + "llm": "jev-1.13.0", + "text_llm": "gemini-3.5-flash-lite", + "start_url": "https://example.com", + "use_custom_api_keys": True, + "api_keys": {"jev": "jev-key", "google": "google-key"}, + "use_computer_action": False, + } + legacy = StartJevComputerUseTaskParams( + task="Find the order", + llm="jev-1.13.0", + text_llm="gemini-3.5-flash-lite", + start_url="https://example.com", + use_custom_api_keys=True, + api_keys=JevComputerUseApiKeys(jev="jev-key", google="google-key"), + use_computer_action=False, + ) + + assert dump_request(mapping, StartJevComputerUseTaskParams) == dump_request( + legacy, + StartJevComputerUseTaskParams, + ) + assert dump_request(mapping, StartJevComputerUseTaskParams) == { + "task": "Find the order", + "llm": "jev-1.13.0", + "textLlm": "gemini-3.5-flash-lite", + "startUrl": "https://example.com", + "useCustomApiKeys": True, + "apiKeys": {"jev": "jev-key", "google": "google-key"}, + "useComputerAction": False, + } + + def test_raw_json_schema_is_copied_without_rewriting_or_dereferencing(): schema = { "$defs": { diff --git a/tests/typecheck/valid_requests.py b/tests/typecheck/valid_requests.py index 08c42e56..fbb4f5dd 100644 --- a/tests/typecheck/valid_requests.py +++ b/tests/typecheck/valid_requests.py @@ -99,7 +99,7 @@ def valid_sync_requests(client: Hyperbrowser) -> None: client.agents.cua.start( { "task": "Complete the task", - "llm": "gpt-5.6-terra", + "llm": "gpt-6-sol", "reasoning_effort": "high", "use_custom_api_keys": True, "api_keys": {"openai": "openai-key"}, @@ -111,10 +111,20 @@ def valid_sync_requests(client: Hyperbrowser) -> None: client.agents.claude_computer_use.start( { "task": "Complete the task", - "llm": "claude-opus-5", + "llm": "claude-opus-5-5", "reasoning_effort": "xhigh", } ) + client.agents.jev_computer_use.start( + { + "task": "Find the order", + "llm": "jev-1.13.0", + "text_llm": "gemini-3.5-flash-lite", + "start_url": "https://example.com", + "use_custom_api_keys": True, + "api_keys": {"jev": "jev-key", "google": "google-key"}, + } + ) client.agents.meta_computer_use.start( { "task": "Complete the task", @@ -264,7 +274,7 @@ async def valid_async_requests(client: AsyncHyperbrowser) -> None: await client.agents.cua.start( { "task": "Complete the task", - "llm": "gpt-5.4", + "llm": "gpt-6-luna", "reasoning_effort": "none", "use_custom_api_keys": True, "api_keys": {"openai": "openai-key"}, @@ -280,6 +290,13 @@ async def valid_async_requests(client: AsyncHyperbrowser) -> None: "reasoning_effort": "max", } ) + await client.agents.jev_computer_use.start( + { + "task": "Find the order", + "llm": "jev-latest", + "text_llm": "gemini-3.5-flash-lite", + } + ) await client.agents.meta_computer_use.start( { "task": "Complete the task", From e1e8d3f81eae72635461bc2aed9b2f153cc5db4c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 22 Sep 2026 19:54:53 +0000 Subject: [PATCH 2/5] Remove use_computer_action from Jev Computer Use start params Jev is CDP-only, so the public start request should not expose this field. Co-authored-by: nikhil --- hyperbrowser/models/agents/jev_computer_use.py | 3 --- hyperbrowser/types/agents.py | 1 - tests/test_request_compat.py | 3 --- 3 files changed, 7 deletions(-) diff --git a/hyperbrowser/models/agents/jev_computer_use.py b/hyperbrowser/models/agents/jev_computer_use.py index fd774efd..1ae1e43d 100644 --- a/hyperbrowser/models/agents/jev_computer_use.py +++ b/hyperbrowser/models/agents/jev_computer_use.py @@ -53,9 +53,6 @@ class StartJevComputerUseTaskParams(BaseModel): api_keys: Optional[JevComputerUseApiKeys] = Field( default=None, serialization_alias="apiKeys" ) - use_computer_action: Optional[bool] = Field( - default=None, serialization_alias="useComputerAction" - ) class StartJevComputerUseTaskResponse(BaseModel): diff --git a/hyperbrowser/types/agents.py b/hyperbrowser/types/agents.py index edc748a0..63edbe6a 100644 --- a/hyperbrowser/types/agents.py +++ b/hyperbrowser/types/agents.py @@ -167,7 +167,6 @@ class StartJevComputerUseTaskParams(TypedDict, total=False): session_options: Optional[CreateSessionParams] use_custom_api_keys: Optional[bool] api_keys: Optional[JevComputerUseApiKeys] - use_computer_action: Optional[bool] class CuaApiKeys(TypedDict, total=False): diff --git a/tests/test_request_compat.py b/tests/test_request_compat.py index 188480be..3f8aea14 100644 --- a/tests/test_request_compat.py +++ b/tests/test_request_compat.py @@ -189,7 +189,6 @@ def test_jev_computer_use_serializes_for_mapping_and_legacy_model(): "start_url": "https://example.com", "use_custom_api_keys": True, "api_keys": {"jev": "jev-key", "google": "google-key"}, - "use_computer_action": False, } legacy = StartJevComputerUseTaskParams( task="Find the order", @@ -198,7 +197,6 @@ def test_jev_computer_use_serializes_for_mapping_and_legacy_model(): start_url="https://example.com", use_custom_api_keys=True, api_keys=JevComputerUseApiKeys(jev="jev-key", google="google-key"), - use_computer_action=False, ) assert dump_request(mapping, StartJevComputerUseTaskParams) == dump_request( @@ -212,7 +210,6 @@ def test_jev_computer_use_serializes_for_mapping_and_legacy_model(): "startUrl": "https://example.com", "useCustomApiKeys": True, "apiKeys": {"jev": "jev-key", "google": "google-key"}, - "useComputerAction": False, } From 0a8c5e81f2c32e3675fda192e16dff7e891c90a7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 22 Sep 2026 19:57:58 +0000 Subject: [PATCH 3/5] Remove start_url from Jev Computer Use start params Keep the public Jev start request aligned with the current supported fields. Co-authored-by: nikhil --- hyperbrowser/models/agents/jev_computer_use.py | 1 - hyperbrowser/types/agents.py | 1 - tests/test_jev_computer_use_models.py | 4 +--- tests/test_request_compat.py | 3 --- tests/typecheck/valid_requests.py | 1 - 5 files changed, 1 insertion(+), 9 deletions(-) diff --git a/hyperbrowser/models/agents/jev_computer_use.py b/hyperbrowser/models/agents/jev_computer_use.py index 1ae1e43d..b5b18278 100644 --- a/hyperbrowser/models/agents/jev_computer_use.py +++ b/hyperbrowser/models/agents/jev_computer_use.py @@ -37,7 +37,6 @@ class StartJevComputerUseTaskParams(BaseModel): task: str llm: Optional[JevComputerUseLlm] = Field(default=None, serialization_alias="llm") text_llm: Optional[JevTextLlm] = Field(default=None, serialization_alias="textLlm") - start_url: Optional[str] = Field(default=None, serialization_alias="startUrl") session_id: Optional[str] = Field(default=None, serialization_alias="sessionId") max_failures: Optional[int] = Field(default=None, serialization_alias="maxFailures") max_steps: Optional[int] = Field(default=None, serialization_alias="maxSteps") diff --git a/hyperbrowser/types/agents.py b/hyperbrowser/types/agents.py index 63edbe6a..bb6592e0 100644 --- a/hyperbrowser/types/agents.py +++ b/hyperbrowser/types/agents.py @@ -159,7 +159,6 @@ class StartJevComputerUseTaskParams(TypedDict, total=False): task: Required[str] llm: Optional[JevComputerUseLlm] text_llm: Optional[JevTextLlm] - start_url: Optional[str] session_id: Optional[str] max_failures: Optional[int] max_steps: Optional[int] diff --git a/tests/test_jev_computer_use_models.py b/tests/test_jev_computer_use_models.py index 54b771e7..50086394 100644 --- a/tests/test_jev_computer_use_models.py +++ b/tests/test_jev_computer_use_models.py @@ -55,13 +55,11 @@ def test_start_jev_computer_use_accepts_api_models(llm: str) -> None: task="Find the order", llm=llm, text_llm="gemini-3.5-flash-lite", - start_url="https://example.com", ) dict_params = { "task": "Find the order", "llm": llm, "text_llm": "gemini-3.5-flash-lite", - "start_url": "https://example.com", } for params in (model_params, dict_params): @@ -69,7 +67,7 @@ def test_start_jev_computer_use_accepts_api_models(llm: str) -> None: assert dumped["task"] == "Find the order" assert dumped["llm"] == llm assert dumped["textLlm"] == "gemini-3.5-flash-lite" - assert dumped["startUrl"] == "https://example.com" + assert "startUrl" not in dumped def test_start_jev_computer_use_rejects_unknown_model() -> None: diff --git a/tests/test_request_compat.py b/tests/test_request_compat.py index 3f8aea14..7c163dc4 100644 --- a/tests/test_request_compat.py +++ b/tests/test_request_compat.py @@ -186,7 +186,6 @@ def test_jev_computer_use_serializes_for_mapping_and_legacy_model(): "task": "Find the order", "llm": "jev-1.13.0", "text_llm": "gemini-3.5-flash-lite", - "start_url": "https://example.com", "use_custom_api_keys": True, "api_keys": {"jev": "jev-key", "google": "google-key"}, } @@ -194,7 +193,6 @@ def test_jev_computer_use_serializes_for_mapping_and_legacy_model(): task="Find the order", llm="jev-1.13.0", text_llm="gemini-3.5-flash-lite", - start_url="https://example.com", use_custom_api_keys=True, api_keys=JevComputerUseApiKeys(jev="jev-key", google="google-key"), ) @@ -207,7 +205,6 @@ def test_jev_computer_use_serializes_for_mapping_and_legacy_model(): "task": "Find the order", "llm": "jev-1.13.0", "textLlm": "gemini-3.5-flash-lite", - "startUrl": "https://example.com", "useCustomApiKeys": True, "apiKeys": {"jev": "jev-key", "google": "google-key"}, } diff --git a/tests/typecheck/valid_requests.py b/tests/typecheck/valid_requests.py index fbb4f5dd..6c2a97ca 100644 --- a/tests/typecheck/valid_requests.py +++ b/tests/typecheck/valid_requests.py @@ -120,7 +120,6 @@ def valid_sync_requests(client: Hyperbrowser) -> None: "task": "Find the order", "llm": "jev-1.13.0", "text_llm": "gemini-3.5-flash-lite", - "start_url": "https://example.com", "use_custom_api_keys": True, "api_keys": {"jev": "jev-key", "google": "google-key"}, } From e49e55abb749b15b41327a79e039d8cbba9f00c4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 22 Sep 2026 20:52:18 +0000 Subject: [PATCH 4/5] Treat Jev Computer Use steps as untyped dicts Leave step payloads as Dict[str, Any] instead of a fixed step schema. Co-authored-by: nikhil --- hyperbrowser/models/__init__.py | 4 -- .../models/agents/jev_computer_use.py | 46 +------------------ tests/test_jev_computer_use_models.py | 10 ++-- 3 files changed, 6 insertions(+), 54 deletions(-) diff --git a/hyperbrowser/models/__init__.py b/hyperbrowser/models/__init__.py index 3aae0e05..7bc21fe0 100644 --- a/hyperbrowser/models/__init__.py +++ b/hyperbrowser/models/__init__.py @@ -140,8 +140,6 @@ ) from .agents.jev_computer_use import ( JevComputerUseTaskStatus, - JevComputerUseAction, - JevComputerUseStepResponse, JevComputerUseTaskData, JevComputerUseTaskResponse, JevComputerUseTaskStatusResponse, @@ -535,8 +533,6 @@ "StartMetaComputerUseTaskParams", "StartMetaComputerUseTaskResponse", "JevComputerUseTaskStatus", - "JevComputerUseAction", - "JevComputerUseStepResponse", "JevComputerUseTaskData", "JevComputerUseTaskResponse", "JevComputerUseTaskStatusResponse", diff --git a/hyperbrowser/models/agents/jev_computer_use.py b/hyperbrowser/models/agents/jev_computer_use.py index b5b18278..16e32350 100644 --- a/hyperbrowser/models/agents/jev_computer_use.py +++ b/hyperbrowser/models/agents/jev_computer_use.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import List, Literal, Optional +from typing import Any, Dict, List, Literal, Optional from pydantic import BaseModel, ConfigDict, Field @@ -79,54 +79,12 @@ class JevComputerUseTaskStatusResponse(BaseModel): status: JevComputerUseTaskStatus -class JevComputerUseAction(BaseModel): - """ - A single Jev action recorded during a task step. - """ - - model_config = ConfigDict( - populate_by_alias=True, - ) - - kind: str - target: Optional[str] = None - option: Optional[str] = None - - -class JevComputerUseStepResponse(BaseModel): - """ - Response from a single Jev Computer Use step. - """ - - model_config = ConfigDict( - populate_by_alias=True, - ) - - step: int - action: JevComputerUseAction - url: str - value: Optional[str] = None - title: Optional[str] = None - page_text: Optional[str] = Field(default=None, alias="pageText") - target_description: Optional[str] = Field(default=None, alias="targetDescription") - confidence: Optional[float] = None - error: Optional[str] = None - error_kind: Optional[str] = Field(default=None, alias="errorKind") - page_changed: Optional[bool] = Field(default=None, alias="pageChanged") - outcome: Optional[str] = None - after: Optional[str] = None - before_tab: Optional[str] = Field(default=None, alias="beforeTab") - after_tab: Optional[str] = Field(default=None, alias="afterTab") - before_observation: Optional[int] = Field(default=None, alias="beforeObservation") - after_observation: Optional[int] = Field(default=None, alias="afterObservation") - - class JevComputerUseTaskData(BaseModel): model_config = ConfigDict( populate_by_alias=True, ) - steps: List[JevComputerUseStepResponse] + steps: List[Dict[str, Any]] final_result: Optional[str] = Field(default=None, alias="finalResult") reached_max_steps: Optional[bool] = Field(default=None, alias="reachedMaxSteps") final_url: Optional[str] = Field(default=None, alias="finalUrl") diff --git a/tests/test_jev_computer_use_models.py b/tests/test_jev_computer_use_models.py index 50086394..39d1b7a9 100644 --- a/tests/test_jev_computer_use_models.py +++ b/tests/test_jev_computer_use_models.py @@ -38,12 +38,10 @@ def test_jev_computer_use_task_response_parses_step_payload() -> None: result = JevComputerUseTaskResponse(**payload) step = result.data.steps[0] - assert step.step == 1 - assert step.action.kind == "click" - assert step.action.target == "submit" - assert step.page_text == "Order details" - assert step.target_description == "Submit button" - assert step.page_changed is True + assert isinstance(step, dict) + assert step["step"] == 1 + assert step["action"] == {"kind": "click", "target": "submit"} + assert step["pageText"] == "Order details" assert result.data.final_result == "Found the order" assert result.data.final_url == "https://example.com/orders/1" assert result.data.reached_max_steps is False From aba3cd394a91bf0159a435614cb8c73da5a69e00 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 22 Sep 2026 20:57:04 +0000 Subject: [PATCH 5/5] Limit Jev Computer Use task data to steps and finalResult Match the Claude Computer Use response shape in the public SDK types. Co-authored-by: nikhil --- hyperbrowser/models/agents/jev_computer_use.py | 3 --- tests/test_jev_computer_use_models.py | 8 +++----- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/hyperbrowser/models/agents/jev_computer_use.py b/hyperbrowser/models/agents/jev_computer_use.py index 16e32350..5fb5b56a 100644 --- a/hyperbrowser/models/agents/jev_computer_use.py +++ b/hyperbrowser/models/agents/jev_computer_use.py @@ -86,9 +86,6 @@ class JevComputerUseTaskData(BaseModel): steps: List[Dict[str, Any]] final_result: Optional[str] = Field(default=None, alias="finalResult") - reached_max_steps: Optional[bool] = Field(default=None, alias="reachedMaxSteps") - final_url: Optional[str] = Field(default=None, alias="finalUrl") - warnings: Optional[List[str]] = None class JevComputerUseTaskMetadata(BaseModel): diff --git a/tests/test_jev_computer_use_models.py b/tests/test_jev_computer_use_models.py index 39d1b7a9..e92122cb 100644 --- a/tests/test_jev_computer_use_models.py +++ b/tests/test_jev_computer_use_models.py @@ -29,9 +29,6 @@ def test_jev_computer_use_task_response_parses_step_payload() -> None: } ], "finalResult": "Found the order", - "reachedMaxSteps": False, - "finalUrl": "https://example.com/orders/1", - "warnings": [], }, } @@ -43,8 +40,9 @@ def test_jev_computer_use_task_response_parses_step_payload() -> None: assert step["action"] == {"kind": "click", "target": "submit"} assert step["pageText"] == "Order details" assert result.data.final_result == "Found the order" - assert result.data.final_url == "https://example.com/orders/1" - assert result.data.reached_max_steps is False + assert not hasattr(result.data, "final_url") + assert not hasattr(result.data, "reached_max_steps") + assert not hasattr(result.data, "warnings") @pytest.mark.parametrize("llm", ["jev-1.13.0", "jev-latest"])