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
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ GROQ_API_KEY=
OPENAI_API_KEY=
OPENROUTER_API_KEY=
PERPLEXITY_API_KEY=
REQUESTY_API_KEY=
TOGETHER_API_KEY=

# --- AWS Bedrock ---
Expand Down
2 changes: 1 addition & 1 deletion AutoCua/android/controller/tool/web/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ use crate::agent::android_use::service::read_text;
/// Providers with no native web-search API: the query goes to the browser
/// agent on the same provider+model instead (see web_agent.rs). Explicit
/// membership, not a catch-all: a typo'd provider must still fail fast below.
const BROWSER_FALLBACK_PROVIDERS: [&str; 3] = ["together", "cerebras", "aws"];
const BROWSER_FALLBACK_PROVIDERS: [&str; 4] = ["together", "cerebras", "requesty", "aws"];

/// AutoCua/android/controller/tool/web: this tool's folder.
fn web_dir(py: Python<'_>) -> PyResult<PathBuf> {
Expand Down
7 changes: 4 additions & 3 deletions AutoCua/android/controller/tool/web/web_agent.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! web_agent.py: the browser-agent fallback for the `web` tool. Providers
//! with no native web-search API (together, cerebras, aws) still have to
//! honour the `web` action, so the query goes to the browser agent
//! with no native web-search API (together, cerebras, requesty, aws) still
//! have to honour the `web` action, so the query goes to the browser agent
//! (AutoCua/web, the Rust/CDP Chrome driver) running on the SAME provider
//! and model; its `done` summary is the web result. The task text below is
//! what turns that summary into a proper report rather than a one-line
Expand Down Expand Up @@ -35,9 +35,10 @@ const SHORT_REPORT_CHARS: usize = 200;
/// The runtime (frontend) key reaches the browser agent through its env: the
/// web agent's __main__ has no --api_key flag, and its .env loader never
/// overrides a var that is already set.
const PROVIDER_ENV_KEY: [(&str, &str); 3] = [
const PROVIDER_ENV_KEY: [(&str, &str); 4] = [
("together", "TOGETHER_API_KEY"),
("cerebras", "CEREBRAS_API_KEY"),
("requesty", "REQUESTY_API_KEY"),
("aws", "AWS_API_KEY"),
];

Expand Down
2 changes: 1 addition & 1 deletion AutoCua/ios/controller/tool/web/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def _web_search(provider: str):
# Providers with no native web-search API: the query goes to the browser
# agent on the same provider+model instead (see web_agent.py). Explicit
# membership, not a catch-all - a typo'd provider must still fail fast below.
_BROWSER_FALLBACK_PROVIDERS = frozenset({"together", "cerebras", "aws"})
_BROWSER_FALLBACK_PROVIDERS = frozenset({"together", "cerebras", "requesty", "aws"})


class WebService:
Expand Down
2 changes: 1 addition & 1 deletion AutoCua/ios/controller/tool/web/web_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
# __main__ has no --api_key flag, and its .env loader never overrides a var
# that is already set.
_PROVIDER_ENV_KEY = {"together": "TOGETHER_API_KEY", "cerebras": "CEREBRAS_API_KEY",
"aws": "AWS_API_KEY"}
"requesty": "REQUESTY_API_KEY", "aws": "AWS_API_KEY"}
# A `done` value shorter than this is almost certainly an activity log, not
# the answer — top it up with what the agent recorded along the way.
_SHORT_REPORT_CHARS = 200
Expand Down
1 change: 1 addition & 0 deletions AutoCua/linux/controller/tool/browser_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"perplexity": "PERPLEXITY_API_KEY",
"together": "TOGETHER_API_KEY",
"cerebras": "CEREBRAS_API_KEY",
"requesty": "REQUESTY_API_KEY",
"aws": "AWS_API_KEY",
}
# A `done` value shorter than this is almost certainly an activity log, so it
Expand Down
2 changes: 1 addition & 1 deletion AutoCua/linux/controller/tool/web/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
# Providers with no native web-search API: the query goes to the browser
# agent on the same provider+model instead (see web_agent.py). Explicit
# membership, not a catch-all — a typo'd provider must still fail fast below.
_BROWSER_FALLBACK_PROVIDERS = frozenset({"together", "cerebras", "aws"})
_BROWSER_FALLBACK_PROVIDERS = frozenset({"together", "cerebras", "requesty", "aws"})


class WebService:
Expand Down
2 changes: 1 addition & 1 deletion AutoCua/linux/controller/tool/web/web_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
# __main__ has no --api_key flag, and its .env loader never overrides a var
# that is already set.
_PROVIDER_ENV_KEY = {"together": "TOGETHER_API_KEY", "cerebras": "CEREBRAS_API_KEY",
"aws": "AWS_API_KEY"}
"requesty": "REQUESTY_API_KEY", "aws": "AWS_API_KEY"}
# A `done` value shorter than this is almost certainly an activity log, not
# the answer — top it up with what the agent recorded along the way.
_SHORT_REPORT_CHARS = 200
Expand Down
1 change: 1 addition & 0 deletions AutoCua/linux/remote_connection/telegram/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
"perplexity": "PERPLEXITY_API_KEY",
"together": "TOGETHER_API_KEY",
"cerebras": "CEREBRAS_API_KEY",
"requesty": "REQUESTY_API_KEY",
"aws": "AWS_API_KEY",
}

Expand Down
1 change: 1 addition & 0 deletions AutoCua/llm_provider/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ def tls_verify():
"perplexity": "https://api.perplexity.ai/",
"together": "https://api.together.xyz/",
"cerebras": "https://api.cerebras.ai/",
"requesty": "https://router.requesty.ai/",
}


Expand Down
9 changes: 9 additions & 0 deletions AutoCua/llm_provider/llm_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"perplexity": "PerplexityProvider",
"together": "TogetherProvider",
"cerebras": "CerebrasProvider",
"requesty": "RequestyProvider",
"aws": "AWSProvider",
}

Expand Down Expand Up @@ -478,6 +479,14 @@ def build_provider(provider: str, model_short_name: str, model_info: dict, tools
return _provider_class("cerebras")(api_key, cli_agent, model_info,
tools=tools,
max_tokens=max_tokens)
elif provider == "requesty":
# Priority: Runtime key > .env fallback
api_key = _key(api_key, 'REQUESTY_API_KEY')
if not api_key:
raise ValueError("Requesty API key not provided, not in .env, and not saved in the app's Settings")
return _provider_class("requesty")(api_key, cli_agent, model_info,
tools=tools,
max_tokens=max_tokens)
elif provider == "aws":
# Priority: Runtime key > .env fallback
api_key = _key(api_key, 'AWS_API_KEY')
Expand Down
4 changes: 4 additions & 0 deletions AutoCua/llm_provider/requesty/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from .service import RequestyProvider
from .view import MODEL_MAPPINGS, get_model_info

__all__ = ['RequestyProvider', 'MODEL_MAPPINGS', 'get_model_info']
109 changes: 109 additions & 0 deletions AutoCua/llm_provider/requesty/service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import json
import requests
from typing import Dict, Any, Optional
from .. import LLM_HTTP_TIMEOUT, screenshots

class RequestyProvider:
"""Requesty API provider for LLM interactions (OpenAI-compatible chat completions)"""

def __init__(self, api_key: str, cli_agent: bool = False, model_info: dict = None, tools: list = None, max_tokens: int = None):
self.api_key = api_key
self.max_tokens = max_tokens
self.api_url = "https://router.requesty.ai/v1/chat/completions"
self.session = requests.Session()
self.cli_agent = cli_agent
self.model_info = model_info or {}
# Native tool calling: OpenAI-format function tools, the model's
# output contract. None only for mode="text" (plain prose).
self.tools = tools or None

def send_request(self, messages: list, model: str, annotated_screenshot_base64: Optional[str] = None) -> Dict[str, Any]:
"""Send request to Requesty API"""

# If screenshot is provided and NOT cli_agent, modify the user message to include the annotated image
if annotated_screenshot_base64 and not self.cli_agent and len(messages) > 1:
user_message = messages[-1]["content"]
messages[-1]["content"] = [
{
"type": "text",
"text": user_message
},
] + [
{
"type": "image_url",
"image_url": {
"url": f"data:image/jpeg;base64,{shot}"
}
}
for shot in screenshots(annotated_screenshot_base64)
]

headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json"
}

data = {
"model": model,
"messages": messages,
"temperature": 0.2,
# Output cap. The handoff manager passes 10k (LLMManager.max_tokens);
# everything else keeps this default.
"max_tokens": self.max_tokens or 10000,
}

# No tools (mode="text") -> plain text: omit the tool params entirely.
if self.tools:
data["tools"] = self.tools
# "required" - every turn must call at least one tool; a text-only
# turn is never a valid step. Canonical rationale: see
# openrouter/service.py.
data["tool_choice"] = self.model_info.get("tool_choice") or "required"

try:
response = self.session.post(self.api_url, json=data, headers=headers, timeout=LLM_HTTP_TIMEOUT)
response.raise_for_status()
result = response.json()
if self.tools:
return _normalize_tool_response(result)
return result
except requests.exceptions.RequestException as e:
error_msg = f"Requesty API request failed: {str(e)}"
if hasattr(e, 'response') and e.response is not None:
error_msg += f"\nResponse: {e.response.text}"
raise Exception(error_msg)


def _normalize_tool_response(result: dict) -> dict:
"""Normalize an OpenAI-format tool-call response: keep the usual
choices/message shape but replace tool_calls with
[{"id", "name", "arguments": dict}] (arguments JSON-decoded). The `id` is
echoed back by the loop so each tool result matches its call."""
message = ((result.get("choices") or [{}])[0].get("message")) or {}
calls = []
for i, tc in enumerate(message.get("tool_calls") or []):
fn = (tc or {}).get("function") or {}
raw_args = fn.get("arguments")
if isinstance(raw_args, dict):
args = raw_args
else:
try:
args = json.loads(raw_args or "{}")
except Exception:
args = {}
if not isinstance(args, dict):
args = {}
calls.append({
"id": str((tc or {}).get("id") or "") or f"call_{i}",
"name": fn.get("name") or "",
"arguments": args,
})
return {
"choices": [{
"message": {
"content": message.get("content") or "",
"tool_calls": calls,
}
}],
"usage": result.get("usage", {}),
}
40 changes: 40 additions & 0 deletions AutoCua/llm_provider/requesty/view.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Model mappings for Requesty provider
# Maps user-friendly names to actual API model names

# Requesty is an OpenAI-compatible router: one key, `vendor/model` ids, the
# same chat/completions shape for every backend. All entries below are
# image+text in / text out with native (OpenAI-format) tool calling, checked
# against https://router.requesty.ai/v1/models. Any other id from that list
# can be typed as the model name and is sent verbatim.
#
# Reasoning is deliberately left unset, the same choice together/ and groq/
# make: with no field on the request each model keeps its own default.
# Temperature is handled once in service.py (0.2).
MODEL_MAPPINGS = {
"claude-sonnet-5": {
"api_name": "anthropic/claude-sonnet-5",
"vision": True,
"display_name": "Claude Sonnet 5"
},
"gemini-3.6-flash": {
"api_name": "google/gemini-3.6-flash",
"vision": True,
"display_name": "Gemini 3.6 Flash"
},
"gpt-5.4": {
"api_name": "openai/gpt-5.4",
"vision": True,
"display_name": "GPT-5.4"
}
}

def get_model_info(short_name: str) -> dict:
"""Get full model information from short name"""
if short_name in MODEL_MAPPINGS:
return MODEL_MAPPINGS[short_name]
# If not found, assume it's already a full model name
return {
"api_name": short_name,
"vision": True,
"display_name": short_name
}
1 change: 1 addition & 0 deletions AutoCua/mac/controller/tool/browser_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"perplexity": "PERPLEXITY_API_KEY",
"together": "TOGETHER_API_KEY",
"cerebras": "CEREBRAS_API_KEY",
"requesty": "REQUESTY_API_KEY",
"aws": "AWS_API_KEY",
}
# A `done` value shorter than this is almost certainly an activity log, so it
Expand Down
2 changes: 1 addition & 1 deletion AutoCua/mac/controller/tool/web/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
# Providers with no native web-search API: the query goes to the browser
# agent on the same provider+model instead (see web_agent.py). Explicit
# membership, not a catch-all — a typo'd provider must still fail fast below.
_BROWSER_FALLBACK_PROVIDERS = frozenset({"together", "cerebras", "aws"})
_BROWSER_FALLBACK_PROVIDERS = frozenset({"together", "cerebras", "requesty", "aws"})


class WebService:
Expand Down
2 changes: 1 addition & 1 deletion AutoCua/mac/controller/tool/web/web_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
# __main__ has no --api_key flag, and its .env loader never overrides a var
# that is already set.
_PROVIDER_ENV_KEY = {"together": "TOGETHER_API_KEY", "cerebras": "CEREBRAS_API_KEY",
"aws": "AWS_API_KEY"}
"requesty": "REQUESTY_API_KEY", "aws": "AWS_API_KEY"}
# A `done` value shorter than this is almost certainly an activity log, not
# the answer — top it up with what the agent recorded along the way.
_SHORT_REPORT_CHARS = 200
Expand Down
1 change: 1 addition & 0 deletions AutoCua/mac/remote_connection/telegram/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
"perplexity": "PERPLEXITY_API_KEY",
"together": "TOGETHER_API_KEY",
"cerebras": "CEREBRAS_API_KEY",
"requesty": "REQUESTY_API_KEY",
"aws": "AWS_API_KEY",
}

Expand Down
2 changes: 1 addition & 1 deletion AutoCua/ui/index.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions AutoCua/ui/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ const PROVIDERS: [string, string, string, string][] = [
['perplexity', 'https://docs.perplexity.ai/guides/getting-started', 'Perplexity', 'pplx-...'],
['together', 'https://api.together.xyz/settings/api-keys', 'Together AI', 'Enter API key...'],
['cerebras', 'https://cloud.cerebras.ai', 'Cerebras', 'csk-...'],
['requesty', 'https://app.requesty.ai/api-keys', 'Requesty', 'Enter API key...'],
['aws', 'https://console.aws.amazon.com/bedrock/home#/api-keys', 'AWS', 'ABSK...'],
['google', 'https://aistudio.google.com/apikey', 'Google', 'AIza...'],
];
Expand Down
1 change: 1 addition & 0 deletions AutoCua/ui/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ document.addEventListener('DOMContentLoaded', () => {
perplexity: null,
together: null,
cerebras: null,
requesty: null,
aws: null
};

Expand Down
7 changes: 7 additions & 0 deletions AutoCua/ui/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -891,6 +891,10 @@ def get_llm_providers():
cerebras_models = importlib.import_module(f"{base}.cerebras.view").MODEL_MAPPINGS
except ModuleNotFoundError:
cerebras_models = None
try:
requesty_models = importlib.import_module(f"{base}.requesty.view").MODEL_MAPPINGS
except ModuleNotFoundError:
requesty_models = None
try:
aws_models = importlib.import_module(f"{base}.aws.view").MODEL_MAPPINGS
except ModuleNotFoundError:
Expand All @@ -914,6 +918,8 @@ def format_models(mappings):
providers.append({'id': 'together', 'name': 'together', 'models': format_models(together_models)})
if cerebras_models:
providers.append({'id': 'cerebras', 'name': 'cerebras', 'models': format_models(cerebras_models)})
if requesty_models:
providers.append({'id': 'requesty', 'name': 'requesty', 'models': format_models(requesty_models)})
if aws_models:
providers.append({'id': 'aws', 'name': 'aws', 'models': format_models(aws_models)})
return providers
Expand All @@ -931,6 +937,7 @@ def format_models(mappings):
'perplexity': 'PERPLEXITY_API_KEY',
'together': 'TOGETHER_API_KEY',
'cerebras': 'CEREBRAS_API_KEY',
'requesty': 'REQUESTY_API_KEY',
'aws': 'AWS_API_KEY',
}

Expand Down
1 change: 1 addition & 0 deletions AutoCua/windows/controller/tool/browser_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"perplexity": "PERPLEXITY_API_KEY",
"together": "TOGETHER_API_KEY",
"cerebras": "CEREBRAS_API_KEY",
"requesty": "REQUESTY_API_KEY",
"aws": "AWS_API_KEY",
}
# A `done` value shorter than this is almost certainly an activity log, so it
Expand Down
2 changes: 1 addition & 1 deletion AutoCua/windows/controller/tool/web/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
# Providers with no native web-search API: the query goes to the browser
# agent on the same provider+model instead (see web_agent.py). Explicit
# membership, not a catch-all - a typo'd provider must still fail fast below.
_BROWSER_FALLBACK_PROVIDERS = frozenset({"together", "cerebras", "aws"})
_BROWSER_FALLBACK_PROVIDERS = frozenset({"together", "cerebras", "requesty", "aws"})


class WebService:
Expand Down
2 changes: 1 addition & 1 deletion AutoCua/windows/controller/tool/web/web_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
# __main__ has no --api_key flag, and its .env loader never overrides a var
# that is already set.
_PROVIDER_ENV_KEY = {"together": "TOGETHER_API_KEY", "cerebras": "CEREBRAS_API_KEY",
"aws": "AWS_API_KEY"}
"requesty": "REQUESTY_API_KEY", "aws": "AWS_API_KEY"}
# A `done` value shorter than this is almost certainly an activity log, not
# the answer — top it up with what the agent recorded along the way.
_SHORT_REPORT_CHARS = 200
Expand Down
1 change: 1 addition & 0 deletions AutoCua/windows/remote_connection/telegram/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
"perplexity": "PERPLEXITY_API_KEY",
"together": "TOGETHER_API_KEY",
"cerebras": "CEREBRAS_API_KEY",
"requesty": "REQUESTY_API_KEY",
"aws": "AWS_API_KEY",
}

Expand Down
Loading