Skip to content
Draft
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
6 changes: 5 additions & 1 deletion crates/browser-use-agent/src/config_overrides.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ impl Default for MultiAgentV2Options {
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ProviderBackend {
Codex,
BrowserUse,
Openai,
Anthropic,
Openrouter,
Expand All @@ -113,6 +114,7 @@ impl ProviderBackend {
pub fn from_provider_id(value: &str) -> Option<Self> {
match value.trim().to_ascii_lowercase().as_str() {
"codex" => Some(Self::Codex),
"browser-use" | "browser_use" | "browseruse" => Some(Self::BrowserUse),
"openai" => Some(Self::Openai),
"anthropic" => Some(Self::Anthropic),
"openrouter" => Some(Self::Openrouter),
Expand Down Expand Up @@ -1957,6 +1959,7 @@ command = "profile-server"
// full variant set matches `browser-use-core::ProviderBackend`.
let all = [
ProviderBackend::Codex,
ProviderBackend::BrowserUse,
ProviderBackend::Openai,
ProviderBackend::Anthropic,
ProviderBackend::Openrouter,
Expand All @@ -1968,6 +1971,7 @@ command = "profile-server"
let name = format!("{backend:?}");
let round_tripped = match name.as_str() {
"Codex" => ProviderBackend::Codex,
"BrowserUse" => ProviderBackend::BrowserUse,
"Openai" => ProviderBackend::Openai,
"Anthropic" => ProviderBackend::Anthropic,
"Openrouter" => ProviderBackend::Openrouter,
Expand All @@ -1978,7 +1982,7 @@ command = "profile-server"
};
assert_eq!(backend, round_tripped);
}
assert_eq!(all.len(), 7);
assert_eq!(all.len(), 8);
}

#[test]
Expand Down
1 change: 1 addition & 0 deletions crates/browser-use-agent/src/entrypoint/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5865,6 +5865,7 @@ mod tests {
id: "call-1".to_string(),
name: "shell".to_string(),
namespace: None,
provider_metadata: None,
input: serde_json::json!({ "command": ["echo", "fusion-ok"] }),
},
LlmEvent::Finish {
Expand Down
61 changes: 61 additions & 0 deletions crates/browser-use-agent/src/entrypoint/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
//! * [`ProviderBackend::Openai`] → [`ProviderChoice::OpenAiResponses`]
//! (key from `OPENAI_API_KEY` / `LLM_BROWSER_OPENAI_API_KEY`, optional
//! `LLM_BROWSER_OPENAI_BASE_URL`),
//! * [`ProviderBackend::BrowserUse`] → [`ProviderChoice::OpenAiCompatibleCustom`]
//! id `"browser-use"` (key from `BROWSER_USE_API_KEY`, base override
//! `LLM_BROWSER_BROWSER_USE_BASE_URL`),
//! * [`ProviderBackend::Anthropic`] → [`ProviderChoice::Anthropic`]
//! (key from `ANTHROPIC_API_KEY` / `LLM_BROWSER_ANTHROPIC_API_KEY`),
//! * [`ProviderBackend::Openrouter`] → [`ProviderChoice::OpenAiCompatibleProvider`]
Expand Down Expand Up @@ -798,6 +801,27 @@ pub fn provider_choice_for_backend(
base_url: env_first(&["LLM_BROWSER_OPENAI_BASE_URL"]),
}))
}
ProviderBackend::BrowserUse => {
let api_key = key_env_then_store(
&["LLM_BROWSER_BROWSER_USE_API_KEY", "BROWSER_USE_API_KEY"],
store,
"browser_use_cloud",
)
.ok_or(ProviderResolveError::MissingCredentials(
"set BROWSER_USE_API_KEY (or run `auth login browser-use-cloud`) for the browser-use backend",
))?;
Ok(Some(ProviderChoice::OpenAiCompatibleCustom {
provider_id: "browser-use".to_string(),
base_url: env_first(&["LLM_BROWSER_BROWSER_USE_BASE_URL"])
.unwrap_or_else(|| "https://llm.api.browser-use.com/v1".to_string()),
api_key,
extra_headers: vec![(
"x-browser-use-request-type".to_string(),
env_first(&["LLM_BROWSER_BROWSER_USE_REQUEST_TYPE"])
.unwrap_or_else(|| "rust_agent".to_string()),
)],
}))
}
ProviderBackend::Anthropic => {
let api_key = key_env_then_store(
&["LLM_BROWSER_ANTHROPIC_API_KEY", "ANTHROPIC_API_KEY"],
Expand Down Expand Up @@ -2704,6 +2728,43 @@ mod tests {
assert!(matches!(resolved, ResolvedProvider::Real(_)));
}

#[test]
fn browser_use_backend_resolves_gateway_route_from_cloud_key() {
let _guard = ENV_LOCK.lock().unwrap();
std::env::remove_var("BROWSER_USE_API_KEY");
std::env::remove_var("LLM_BROWSER_BROWSER_USE_API_KEY");
let dir = tempfile::tempdir().expect("tempdir");
let store = Store::open(dir.path()).expect("store");
store
.set_setting("auth.browser_use_cloud.api_key", "stored-browser-use-key")
.unwrap();

let choice = provider_choice_for_backend(ProviderBackend::BrowserUse, Some(&store))
.expect("resolves")
.expect("browser-use is a real provider");

match choice {
ProviderChoice::OpenAiCompatibleCustom {
provider_id,
base_url,
api_key,
extra_headers,
} => {
assert_eq!(provider_id, "browser-use");
assert_eq!(base_url, "https://llm.api.browser-use.com/v1");
assert_eq!(api_key, "stored-browser-use-key");
assert_eq!(
extra_headers,
vec![(
"x-browser-use-request-type".to_string(),
"rust_agent".to_string()
)]
);
}
other => panic!("expected browser-use gateway choice, got {other:?}"),
}
}

/// A real Anthropic backend also constructs offline given its key.
#[test]
fn resolves_real_anthropic_driver_offline() {
Expand Down
1 change: 1 addition & 0 deletions crates/browser-use-agent/src/events/map_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ fn tool_call_maps_to_tool_started_with_parsed_arguments() {
id: "c0".to_string(),
name: "click".to_string(),
namespace: None,
provider_metadata: None,
input: json!({ "index": 5 }),
},
);
Expand Down
1 change: 1 addition & 0 deletions crates/browser-use-agent/src/turn/fusion_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ fn tool_call_ev(id: &str, name: &str, input: serde_json::Value) -> LlmEvent {
id: id.to_string(),
name: name.to_string(),
namespace: None,
provider_metadata: None,
input,
}
}
Expand Down
47 changes: 45 additions & 2 deletions crates/browser-use-agent/src/turn/model_path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ use browser_use_llm::auth::{codex_route, CodexAuth};
use browser_use_llm::providers::{
Anthropic, AnthropicConfig, OpenAi, OpenAiCompatible, OpenAiConfig,
};
use browser_use_llm::route::{ModelClient, Route};
use browser_use_llm::route::{Auth, ModelClient, Route};
use browser_use_llm::schema::{ContentPart, LlmRequest, Message, MessageRole, SystemPart};

use crate::events::{EventSink, TurnCtx};
Expand Down Expand Up @@ -71,6 +71,8 @@ pub enum ProviderChoice {
base_url: String,
/// API key.
api_key: String,
/// Additional static headers to apply to every request for this route.
extra_headers: Vec<(String, String)>,
},
/// The codex (chatgpt.com) backend, reached via the Codex CLI OAuth login.
///
Expand Down Expand Up @@ -166,6 +168,7 @@ pub fn provider_choice_from_env() -> Result<ProviderChoice, ModelPathError> {
provider_id: "openai-compatible".to_string(),
base_url,
api_key,
extra_headers: Vec::new(),
});
}
Err(ModelPathError::MissingCredentials(
Expand Down Expand Up @@ -202,10 +205,17 @@ pub fn build_route(choice: &ProviderChoice, model: &str) -> Result<Route, ModelP
provider_id,
base_url,
api_key,
extra_headers,
} => {
let provider =
OpenAiCompatible::configure(provider_id.clone(), base_url.clone(), api_key.clone());
Ok(provider.chat(model))
let mut route = provider.chat(model);
for (name, value) in extra_headers {
route.auth = route
.auth
.and_then(Auth::header(name.clone(), value.clone()));
}
Ok(route)
}
ProviderChoice::Codex {
access_token,
Expand Down Expand Up @@ -249,9 +259,12 @@ pub fn build_transport(
),
);
}
apply_browser_use_provider_options(&ctx.provider, &mut req);
ModelClientTransport::new(client, route, req)
}

pub(crate) fn apply_browser_use_provider_options(_provider: &str, _req: &mut LlmRequest) {}

/// Build the production text-only [`ModelSamplingDriver`] over a live transport.
///
/// This is the real [`SamplingDriver`](crate::turn::SamplingDriver) the turn loop
Expand Down Expand Up @@ -353,6 +366,7 @@ mod tests {
provider_id: "internal".to_string(),
base_url: "https://llm.internal/v1".to_string(),
api_key: "k".to_string(),
extra_headers: Vec::new(),
};
let route = build_route(&choice, "m").unwrap();
assert_eq!(
Expand All @@ -361,6 +375,34 @@ mod tests {
);
}

#[test]
fn openai_compatible_custom_applies_extra_headers() {
let choice = ProviderChoice::OpenAiCompatibleCustom {
provider_id: "browser-use".to_string(),
base_url: "https://llm.api.browser-use.com/v1".to_string(),
api_key: "k".to_string(),
extra_headers: vec![(
"x-browser-use-request-type".to_string(),
"rust_agent".to_string(),
)],
};
let route = build_route(&choice, "bu-3-max").unwrap();

assert_eq!(
header(&route, "x-browser-use-request-type").as_deref(),
Some("rust_agent")
);
}

#[test]
fn browser_use_provider_options_do_not_tag_request_body() {
let mut req = LlmRequest::new("bu-3-max", "browseruse");

apply_browser_use_provider_options("browser-use", &mut req);

assert_eq!(req.provider_options, None);
}

/// Only the `Codex` variant targets chatgpt.com: the env-keyed providers never
/// route to the codex backend, while `Codex` does (and only it).
#[test]
Expand All @@ -379,6 +421,7 @@ mod tests {
provider_id: "x".into(),
base_url: "https://llm.internal/v1".into(),
api_key: "k".into(),
extra_headers: Vec::new(),
},
] {
let url = build_route(&choice, "m").unwrap().endpoint.url();
Expand Down
7 changes: 5 additions & 2 deletions crates/browser-use-agent/src/turn/sampling.rs
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,7 @@ impl<T: SamplingTransport, R: CallRunner + 'static> ModelSamplingDriver<T, R> {
id,
name,
namespace,
provider_metadata,
input,
} => {
// Capture the actual call (model order) so the fused dispatch can
Expand All @@ -611,8 +612,9 @@ impl<T: SamplingTransport, R: CallRunner + 'static> ModelSamplingDriver<T, R> {
id,
name,
input,
provider_metadata: namespace
.map(|namespace| serde_json::json!({ "namespace": namespace })),
provider_metadata: provider_metadata.or_else(|| {
namespace.map(|namespace| serde_json::json!({ "namespace": namespace }))
}),
});
Ok(StreamProgress::Continue)
}
Expand Down Expand Up @@ -1083,6 +1085,7 @@ fn build_request(ctx: &TurnCtx, input: Vec<Message>) -> LlmRequest {
),
);
}
super::model_path::apply_browser_use_provider_options(&ctx.provider, &mut req);
mark_message_cache_breakpoints(&mut req.messages);
req
}
Expand Down
2 changes: 2 additions & 0 deletions crates/browser-use-agent/src/turn/sampling_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ fn tool_call(name: &str) -> Result<LlmEvent, LlmError> {
id: "call-1".to_string(),
name: name.to_string(),
namespace: None,
provider_metadata: None,
input: serde_json::json!({"arg": 1}),
})
}
Expand All @@ -224,6 +225,7 @@ fn tool_call_with_input(name: &str, input: serde_json::Value) -> Result<LlmEvent
id: "call-1".to_string(),
name: name.to_string(),
namespace: None,
provider_metadata: None,
input,
})
}
Expand Down
Loading
Loading