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
28 changes: 21 additions & 7 deletions src/reverse_api/browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ def _null_logger(message: dict) -> None:
"""Null logger that discards all messages."""
pass


# Realistic Chrome user agents (updated for late 2024/2025)
USER_AGENTS = [
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
Expand Down Expand Up @@ -593,7 +594,8 @@ def __init__(
prompt: str,
output_dir: str | None = None,
timeout: int = 300,
agent_model: str = "bu-llm",
browser_use_model: str = "bu-llm",
stagehand_model: str = "openai/computer-use-preview-2025-03-11",
agent_provider: str = "browser-use",
start_url: Optional[str] = None,
):
Expand All @@ -602,7 +604,8 @@ def __init__(
self.prompt = prompt
self.output_dir = output_dir
self.timeout = timeout
self.agent_model = agent_model
self.browser_use_model = browser_use_model
self.stagehand_model = stagehand_model
self.agent_provider = agent_provider
self.start_url = start_url

Expand All @@ -616,12 +619,21 @@ def __init__(

def _save_metadata(self, end_time: str, result: dict | None = None) -> None:
"""Save run metadata to JSON file."""
# Select the appropriate model based on agent_provider
agent_model = (
self.stagehand_model
if self.agent_provider == "stagehand"
else self.browser_use_model
)

metadata = {
"run_id": self.run_id,
"prompt": self.prompt,
"mode": "agent",
"agent_provider": self.agent_provider,
"agent_model": self.agent_model,
"agent_model": agent_model,
"browser_use_model": self.browser_use_model,
"stagehand_model": self.stagehand_model,
"start_time": self._start_time,
"end_time": end_time,
"har_file": str(self.har_path),
Expand Down Expand Up @@ -709,7 +721,7 @@ def emit(self, record):
# Parse agent model and validate API key
try:
provider, model_name = parse_agent_model(
self.agent_model, self.agent_provider
self.browser_use_model, self.agent_provider
)
except ValueError as e:
result["error"] = str(e)
Expand Down Expand Up @@ -842,7 +854,7 @@ async def _run_with_stagehand(self) -> dict:
try:
try:
provider, model_name = parse_agent_model(
self.agent_model, self.agent_provider
self.stagehand_model, self.agent_provider
)
except ValueError as e:
result["error"] = str(e)
Expand Down Expand Up @@ -1052,7 +1064,8 @@ def run_agent_browser(
prompt: str,
output_dir: str | None = None,
timeout: int = 300,
agent_model: str = "bu-llm",
browser_use_model: str = "bu-llm",
stagehand_model: str = "openai/computer-use-preview-2025-03-11",
agent_provider: str = "browser-use",
start_url: Optional[str] = None,
) -> Path:
Expand All @@ -1062,7 +1075,8 @@ def run_agent_browser(
prompt=prompt,
output_dir=output_dir,
timeout=timeout,
agent_model=agent_model,
browser_use_model=browser_use_model,
stagehand_model=stagehand_model,
agent_provider=agent_provider,
start_url=start_url,
)
Expand Down
207 changes: 149 additions & 58 deletions src/reverse_api/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,8 @@ def get_prompt():
return {
"mode": result_mode,
"run_id": prompt,
"model": model or config_manager.get("model", "claude-sonnet-4-5"),
"model": model
or config_manager.get("claude_code_model", "claude-sonnet-4-5"),
}

# Agent mode: similar to manual but uses autonomous browser
Expand All @@ -174,7 +175,7 @@ def get_prompt():
raise click.Abort()

if model is None:
model = config_manager.get("model", "claude-sonnet-4-5")
model = config_manager.get("claude_code_model", "claude-sonnet-4-5")

return {
"mode": result_mode,
Expand Down Expand Up @@ -208,7 +209,7 @@ def get_prompt():
reverse_engineer = True

if model is None:
model = config_manager.get("model", "claude-sonnet-4-5")
model = config_manager.get("claude_code_model", "claude-sonnet-4-5")

return {
"mode": result_mode,
Expand Down Expand Up @@ -315,10 +316,13 @@ def handle_settings():
action = questionary.select(
"",
choices=[
Choice(title="> change model", value="model"),
Choice(title="> change claude code model", value="claude_code_model"),
Choice(title="> change sdk", value="sdk"),
Choice(title="> opencode provider", value="opencode_provider"),
Choice(title="> opencode model", value="opencode_model"),
Choice(title="> agent provider", value="agent_provider"),
Choice(title="> agent model", value="agent_model"),
Choice(title="> browser-use model", value="browser_use_model"),
Choice(title="> stagehand model", value="stagehand_model"),
Choice(title="> output directory", value="output_dir"),
Choice(title="> back", value="back"),
],
Expand All @@ -335,7 +339,7 @@ def handle_settings():
if action is None or action == "back":
return

if action == "model":
if action == "claude_code_model":
model_choices = [
Choice(title=f"> {c['name'].lower()}", value=c["value"])
for c in get_model_choices()
Expand All @@ -353,7 +357,7 @@ def handle_settings():
),
).ask()
if model and model != "back":
config_manager.set("model", model)
config_manager.set("claude_code_model", model)
console.print(f" [dim]updated[/dim] {model}\n")

elif action == "sdk":
Expand Down Expand Up @@ -397,35 +401,109 @@ def handle_settings():
if provider and provider != "back":
config_manager.set("agent_provider", provider)
console.print(f" [dim]updated[/dim] agent provider: {provider}\n")
# If switching to stagehand, validate current model
if provider == "stagehand":
current_model = config_manager.get("agent_model", "bu-llm")
try:
from .browser import parse_agent_model

parse_agent_model(current_model, provider)
except ValueError:
elif action == "opencode_provider":
current = config_manager.get("opencode_provider", "anthropic")
new_provider = questionary.text(
" > opencode provider",
default=current or "anthropic",
instruction="(e.g., 'anthropic', 'openai', 'google')",
qmark="",
style=questionary.Style(
[
("question", f"fg:{THEME_SECONDARY}"),
("instruction", f"fg:{THEME_DIM} italic"),
]
),
).ask()
if new_provider is not None:
new_provider = new_provider.strip()
if not new_provider:
console.print(
" [yellow]error:[/yellow] opencode provider cannot be empty\n"
)
else:
config_manager.set("opencode_provider", new_provider)
console.print(
f" [dim]updated[/dim] opencode provider: {new_provider}\n"
)

elif action == "opencode_model":
current = config_manager.get("opencode_model", "claude-sonnet-4-5")
new_model = questionary.text(
" > opencode model",
default=current or "claude-sonnet-4-5",
instruction="(e.g., 'claude-sonnet-4-5', 'claude-opus-4-5')",
qmark="",
style=questionary.Style(
[
("question", f"fg:{THEME_SECONDARY}"),
("instruction", f"fg:{THEME_DIM} italic"),
]
),
).ask()
if new_model is not None:
new_model = new_model.strip()
if not new_model:
console.print(
" [yellow]error:[/yellow] opencode model cannot be empty\n"
)
else:
config_manager.set("opencode_model", new_model)
console.print(f" [dim]updated[/dim] opencode model: {new_model}\n")

elif action == "browser_use_model":
from .browser import parse_agent_model

current = config_manager.get("browser_use_model", "bu-llm")
instruction = "(Format: 'bu-llm' or 'provider/model', e.g., 'openai/gpt-4')"

new_model = questionary.text(
" > browser-use model",
default=current or "bu-llm",
instruction=instruction,
qmark="",
style=questionary.Style(
[
("question", f"fg:{THEME_SECONDARY}"),
("instruction", f"fg:{THEME_DIM} italic"),
]
),
).ask()
if new_model is not None:
new_model = new_model.strip()
if not new_model:
console.print(
" [yellow]error:[/yellow] browser-use model cannot be empty\n"
)
else:
# Validate format for browser-use
try:
parse_agent_model(new_model, "browser-use")
config_manager.set("browser_use_model", new_model)
console.print(
f" [yellow]warning:[/yellow] Current agent model '{current_model}' may not be compatible with stagehand.\n"
f" [dim]updated[/dim] browser-use model: {new_model}\n"
)
except ValueError as e:
console.print(f" [yellow]error:[/yellow] {e}\n")
console.print(
f" [dim]Stagehand supports OpenAI and Anthropic Computer Use models[/dim]\n"
f" [dim]Examples: openai/computer-use-preview-2025-03-11, anthropic/claude-sonnet-4-5-20250929[/dim]\n"
" [dim]Valid formats:[/dim]\n"
" [dim] - bu-llm[/dim]\n"
" [dim] - openai/model_name (e.g., openai/gpt-4)[/dim]\n"
" [dim] - google/model_name (e.g., google/gemini-pro)[/dim]\n"
)

elif action == "agent_model":
elif action == "stagehand_model":
from .browser import parse_agent_model

current = config_manager.get("agent_model", "bu-llm")
agent_provider = config_manager.get("agent_provider", "browser-use")

instruction = "(Format: 'bu-llm' or 'provider/model', e.g., 'openai/gpt-4')"
if agent_provider == "stagehand":
instruction = "(Format: 'openai/model' or 'anthropic/model', e.g., 'openai/computer-use-preview-2025-03-11' or 'anthropic/claude-sonnet-4-5-20250929')"
current = config_manager.get(
"stagehand_model", "openai/computer-use-preview-2025-03-11"
)
instruction = "(Format: 'openai/model' or 'anthropic/model', e.g., 'openai/computer-use-preview-2025-03-11' or 'anthropic/claude-sonnet-4-5-20250929')"

new_model = questionary.text(
" > agent model",
default=current or "bu-llm",
" > stagehand model",
default=current or "openai/computer-use-preview-2025-03-11",
instruction=instruction,
qmark="",
style=questionary.Style(
Expand All @@ -438,30 +516,24 @@ def handle_settings():
if new_model is not None:
new_model = new_model.strip()
if not new_model:
console.print(" [yellow]error:[/yellow] agent model cannot be empty\n")
console.print(
" [yellow]error:[/yellow] stagehand model cannot be empty\n"
)
else:
# Validate format with current agent_provider
# Validate format for stagehand
try:
parse_agent_model(new_model, agent_provider)
config_manager.set("agent_model", new_model)
console.print(f" [dim]updated[/dim] agent model: {new_model}\n")
parse_agent_model(new_model, "stagehand")
config_manager.set("stagehand_model", new_model)
console.print(f" [dim]updated[/dim] stagehand model: {new_model}\n")
except ValueError as e:
console.print(f" [yellow]error:[/yellow] {e}\n")
if agent_provider == "stagehand":
console.print(
" [dim]Valid formats for stagehand:[/dim]\n"
" [dim] - openai/computer-use-preview-2025-03-11[/dim]\n"
" [dim] - anthropic/claude-sonnet-4-5-20250929[/dim]\n"
" [dim] - anthropic/claude-haiku-4-5-20251001[/dim]\n"
" [dim] - anthropic/claude-opus-4-5-20251101[/dim]\n"
)
else:
console.print(
" [dim]Valid formats:[/dim]\n"
" [dim] - bu-llm[/dim]\n"
" [dim] - openai/model_name (e.g., openai/gpt-4)[/dim]\n"
" [dim] - google/model_name (e.g., google/gemini-pro)[/dim]\n"
)
console.print(
" [dim]Valid formats for stagehand:[/dim]\n"
" [dim] - openai/computer-use-preview-2025-03-11[/dim]\n"
" [dim] - anthropic/claude-sonnet-4-5-20250929[/dim]\n"
" [dim] - anthropic/claude-haiku-4-5-20251001[/dim]\n"
" [dim] - anthropic/claude-opus-4-5-20251101[/dim]\n"
)

elif action == "output_dir":
current = config_manager.get("output_dir")
Expand Down Expand Up @@ -518,7 +590,9 @@ def handle_history():
if run:
console.print(Panel(json.dumps(run, indent=2), border_style=THEME_DIM))
if questionary.confirm(" > recode?").ask():
model = run.get("model") or config_manager.get("model", "claude-sonnet-4-5")
model = run.get("model") or config_manager.get(
"claude_code_model", "claude-sonnet-4-5"
)
run_engineer(run_id, model=model)
else:
console.print(" [dim]> not found[/dim]")
Expand Down Expand Up @@ -692,8 +766,11 @@ def run_agent_capture(
run_id = generate_run_id()
timestamp = get_timestamp()

# Get agent model and provider from config
agent_model = config_manager.get("agent_model", "bu-llm")
# Get agent models and provider from config
browser_use_model = config_manager.get("browser_use_model", "bu-llm")
stagehand_model = config_manager.get(
"stagehand_model", "openai/computer-use-preview-2025-03-11"
)
agent_provider = config_manager.get("agent_provider", "browser-use")

# Record initial session
Expand All @@ -713,7 +790,8 @@ def run_agent_capture(
run_id=run_id,
prompt=prompt,
output_dir=output_dir,
agent_model=agent_model,
browser_use_model=browser_use_model,
stagehand_model=stagehand_model,
agent_provider=agent_provider,
start_url=url,
)
Expand Down Expand Up @@ -813,14 +891,27 @@ def run_engineer(run_id, har_path=None, prompt=None, model=None, output_dir=None
har_dir = Path(paths.get("har_dir", get_har_dir(run_id, None)))
har_path = har_dir / "recording.har"

result = run_reverse_engineering(
run_id=run_id,
har_path=har_path,
prompt=prompt,
model=model or config_manager.get("model", "claude-sonnet-4-5"),
output_dir=output_dir,
sdk=config_manager.get("sdk", "opencode"),
)
sdk = config_manager.get("sdk", "claude")
if sdk == "opencode":
result = run_reverse_engineering(
run_id=run_id,
har_path=har_path,
prompt=prompt,
model=model,
output_dir=output_dir,
sdk=sdk,
opencode_provider=config_manager.get("opencode_provider", "anthropic"),
opencode_model=config_manager.get("opencode_model", "claude-sonnet-4-5"),
)
else:
result = run_reverse_engineering(
run_id=run_id,
har_path=har_path,
prompt=prompt,
model=model or config_manager.get("claude_code_model", "claude-sonnet-4-5"),
output_dir=output_dir,
sdk=sdk,
)

if result:
# Automatically copy scripts to current directory with a readable name
Expand Down
Loading