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
6 changes: 3 additions & 3 deletions codex-rs/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions codex-rs/app-server/src/codex_message_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1675,6 +1675,7 @@ async fn derive_config_from_params(
tools_web_search_request: None,
experimental_sandbox_command_assessment: None,
additional_writable_roots: Vec::new(),
browseros_config: None,
};

let cli_overrides = cli_overrides
Expand Down
111 changes: 102 additions & 9 deletions codex-rs/core/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1162,6 +1162,8 @@ pub struct ConfigOverrides {
pub experimental_sandbox_command_assessment: Option<bool>,
/// Additional directories that should be treated as writable roots for this session.
pub additional_writable_roots: Vec<PathBuf>,
/// Path to BrowserOS provider config file (TOML).
pub browseros_config: Option<PathBuf>,
}

impl Config {
Expand Down Expand Up @@ -1191,6 +1193,7 @@ impl Config {
tools_web_search_request: override_tools_web_search_request,
experimental_sandbox_command_assessment: sandbox_command_assessment_override,
additional_writable_roots,
browseros_config,
} = overrides;

let active_profile_name = config_profile_key
Expand Down Expand Up @@ -1292,10 +1295,85 @@ impl Config {
model_providers.entry(key).or_insert(provider);
}

let model_provider_id = model_provider
.or(config_profile.model_provider)
.or(cfg.model_provider)
.unwrap_or_else(|| "openai".to_string());
// Load BrowserOS provider if config file is specified.
let mut browseros_model_name = None;
let mut browseros_mcp_servers = HashMap::new();
let mut browseros_base_instructions_file = None;
if let Some(browseros_config_path) = &browseros_config {
use crate::model_provider_info::load_browseros_provider;
use crate::model_provider_info::BUILT_IN_BROWSEROS_MODEL_PROVIDER_ID;
use crate::model_provider_info::BrowserOSConfig;

// Load the full BrowserOS config to extract model_name, MCP servers, and base_instructions_file
let full_path = if browseros_config_path.is_relative() {
resolved_cwd.join(browseros_config_path)
} else {
browseros_config_path.clone()
};
let contents = std::fs::read_to_string(&full_path).map_err(|e| {
std::io::Error::new(
e.kind(),
format!("failed to read BrowserOS config file {}: {e}", full_path.display()),
)
})?;
let browseros_cfg: BrowserOSConfig = toml::from_str(&contents).map_err(|e| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!(
"failed to parse BrowserOS config file {}: {e}",
full_path.display()
),
)
})?;

// Extract model_name for later use
browseros_model_name = browseros_cfg.model_name.clone();

// Extract base_instructions_file path for later use
// Resolve relative paths relative to the BrowserOS config file's directory
if let Some(instructions_path) = browseros_cfg.base_instructions_file {
let resolved_instructions_path = if instructions_path.is_relative() {
full_path
.parent()
.unwrap_or(&resolved_cwd)
.join(&instructions_path)
} else {
instructions_path
};
browseros_base_instructions_file = Some(resolved_instructions_path);
}

// Extract MCP servers from BrowserOS config
if let Some(mcp_servers_value) = browseros_cfg.mcp_servers {
for (name, server_value) in mcp_servers_value {
// Deserialize MCP server config from TOML value
match toml::from_str::<McpServerConfig>(&toml::to_string(&server_value).unwrap_or_default()) {
Ok(server_config) => {
browseros_mcp_servers.insert(name, server_config);
}
Err(e) => {
tracing::warn!(
"Failed to parse MCP server '{name}' from BrowserOS config: {e}"
);
}
}
}
}

// Load and register the BrowserOS provider
let browseros_provider = load_browseros_provider(browseros_config_path, &resolved_cwd)?;
model_providers.insert(BUILT_IN_BROWSEROS_MODEL_PROVIDER_ID.to_string(), browseros_provider);
}

// Determine model provider ID: BrowserOS override takes precedence if configured
let model_provider_id = if browseros_config.is_some() {
crate::model_provider_info::BUILT_IN_BROWSEROS_MODEL_PROVIDER_ID.to_string()
} else {
model_provider
.or(config_profile.model_provider)
.or(cfg.model_provider)
.unwrap_or_else(|| "openai".to_string())
};
let model_provider = model_providers
.get(&model_provider_id)
.ok_or_else(|| {
Expand Down Expand Up @@ -1331,7 +1409,9 @@ impl Config {

let forced_login_method = cfg.forced_login_method;

// Model resolution: BrowserOS config model_name takes precedence if provided
let model = model
.or(browseros_model_name)
.or(config_profile.model)
.or(cfg.model)
.unwrap_or_else(default_model);
Expand Down Expand Up @@ -1361,16 +1441,22 @@ impl Config {
.and_then(|info| info.auto_compact_token_limit)
});

// Load base instructions override from a file if specified. If the
// path is relative, resolve it against the effective cwd so the
// behaviour matches other path-like config values.
// Load base instructions override from a file if specified. Precedence:
// 1. CLI override (base_instructions from ConfigOverrides) - highest
// 2. BrowserOS config base_instructions_file
// 3. config.toml experimental_instructions_file
// 4. Model family default - lowest
let experimental_instructions_path = config_profile
.experimental_instructions_file
.as_ref()
.or(cfg.experimental_instructions_file.as_ref());
let file_base_instructions =
Self::get_base_instructions(experimental_instructions_path, &resolved_cwd)?;
let base_instructions = base_instructions.or(file_base_instructions);
let browseros_base_instructions =
Self::get_base_instructions(browseros_base_instructions_file.as_ref(), &resolved_cwd)?;
let base_instructions = base_instructions
.or(browseros_base_instructions)
.or(file_base_instructions);

// Default review model when not set in config; allow CLI override to take precedence.
let review_model = override_review_model
Expand Down Expand Up @@ -1398,7 +1484,14 @@ impl Config {
// The config.toml omits "_mode" because it's a config file. However, "_mode"
// is important in code to differentiate the mode from the store implementation.
cli_auth_credentials_store_mode: cfg.cli_auth_credentials_store.unwrap_or_default(),
mcp_servers: cfg.mcp_servers,
mcp_servers: {
// Merge MCP servers: BrowserOS config MCP servers take precedence over global config
let mut merged_mcp_servers = cfg.mcp_servers;
for (name, server_config) in browseros_mcp_servers {
merged_mcp_servers.insert(name, server_config);
}
merged_mcp_servers
},
// The config.toml omits "_mode" because it's a config file. However, "_mode"
// is important in code to differentiate the mode from the store implementation.
mcp_oauth_credentials_store_mode: cfg.mcp_oauth_credentials_store.unwrap_or_default(),
Expand Down
1 change: 1 addition & 0 deletions codex-rs/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ pub mod token_data;
mod truncate;
mod unified_exec;
mod user_instructions;
pub use model_provider_info::BUILT_IN_BROWSEROS_MODEL_PROVIDER_ID;
pub use model_provider_info::BUILT_IN_OSS_MODEL_PROVIDER_ID;
pub use model_provider_info::ModelProviderInfo;
pub use model_provider_info::WireApi;
Expand Down
100 changes: 99 additions & 1 deletion codex-rs/core/src/model_provider_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use serde::Deserialize;
use serde::Serialize;
use std::collections::HashMap;
use std::env::VarError;
use std::path::PathBuf;
use std::time::Duration;

use crate::error::EnvVarError;
Expand Down Expand Up @@ -261,6 +262,7 @@ impl ModelProviderInfo {
const DEFAULT_OLLAMA_PORT: u32 = 11434;

pub const BUILT_IN_OSS_MODEL_PROVIDER_ID: &str = "oss";
pub const BUILT_IN_BROWSEROS_MODEL_PROVIDER_ID: &str = "browseros";

/// Built-in default provider list.
pub fn built_in_model_providers() -> HashMap<String, ModelProviderInfo> {
Expand All @@ -286,7 +288,7 @@ pub fn built_in_model_providers() -> HashMap<String, ModelProviderInfo> {
env_key: None,
env_key_instructions: None,
experimental_bearer_token: None,
wire_api: WireApi::Chat,
wire_api: WireApi::Responses,
query_params: None,
http_headers: Some(
[("version".to_string(), env!("CARGO_PKG_VERSION").to_string())]
Expand Down Expand Up @@ -357,6 +359,102 @@ pub fn create_oss_provider_with_base_url(base_url: &str) -> ModelProviderInfo {
}
}

/// BrowserOS provider configuration loaded from a TOML file.
#[derive(Debug, Clone, Deserialize, PartialEq)]
pub struct BrowserOSConfig {
/// Model name to use (e.g., "gpt-4o", "o3").
pub model_name: Option<String>,
/// Base URL for the API. If not set, will try BROWSEROS_BASE_URL env var.
pub base_url: Option<String>,
/// Environment variable name that contains the API key.
/// If not set, will try BROWSEROS_API_KEY env var.
pub api_key_env: Option<String>,
/// Wire API protocol to use: "chat" or "responses".
#[serde(default)]
pub wire_api: WireApi,
/// Additional query parameters for the base URL.
pub query_params: Option<HashMap<String, String>>,
/// Additional HTTP headers (static values).
pub http_headers: Option<HashMap<String, String>>,
/// HTTP headers sourced from environment variables.
pub env_http_headers: Option<HashMap<String, String>>,
/// Maximum number of request retries.
pub request_max_retries: Option<u64>,
/// Maximum number of stream reconnection attempts.
pub stream_max_retries: Option<u64>,
/// Idle timeout for streaming responses (milliseconds).
pub stream_idle_timeout_ms: Option<u64>,
/// MCP servers to enable (same structure as config.toml mcp_servers).
#[serde(default)]
pub mcp_servers: Option<HashMap<String, toml::Value>>,
/// Path to a file containing base instructions (system prompt) for the LLM.
/// If relative, will be resolved relative to the BrowserOS config file's directory.
pub base_instructions_file: Option<PathBuf>,
}

/// Load BrowserOS provider configuration from a TOML file.
pub fn load_browseros_provider(
config_path: &std::path::Path,
cwd: &std::path::Path,
) -> std::io::Result<ModelProviderInfo> {
// Resolve relative paths against cwd
let full_path = if config_path.is_relative() {
cwd.join(config_path)
} else {
config_path.to_path_buf()
};

let contents = std::fs::read_to_string(&full_path).map_err(|e| {
std::io::Error::new(
e.kind(),
format!("failed to read BrowserOS config file {}: {e}", full_path.display()),
)
})?;

let browseros_cfg: BrowserOSConfig = toml::from_str(&contents).map_err(|e| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!(
"failed to parse BrowserOS config file {}: {e}",
full_path.display()
),
)
})?;

// Resolve base_url: use config value, then env var, then error
let base_url = browseros_cfg.base_url.or_else(|| {
std::env::var("BROWSEROS_BASE_URL")
.ok()
.filter(|v| !v.trim().is_empty())
}).ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
"BrowserOS config must specify base_url or set BROWSEROS_BASE_URL environment variable",
)
})?;

// Resolve API key env var name
let api_key_env = browseros_cfg
.api_key_env
.unwrap_or_else(|| "BROWSEROS_API_KEY".to_string());

Ok(ModelProviderInfo {
name: "BrowserOS".into(),
base_url: Some(base_url),
env_key: Some(api_key_env),
env_key_instructions: None,
experimental_bearer_token: None,
wire_api: browseros_cfg.wire_api,
query_params: browseros_cfg.query_params,
http_headers: browseros_cfg.http_headers,
env_http_headers: browseros_cfg.env_http_headers,
request_max_retries: browseros_cfg.request_max_retries,
stream_max_retries: browseros_cfg.stream_max_retries,
stream_idle_timeout_ms: browseros_cfg.stream_idle_timeout_ms,
requires_openai_auth: false,
})
}

fn matches_azure_responses_base_url(base_url: &str) -> bool {
let base = base_url.to_ascii_lowercase();
const AZURE_MARKERS: [&str; 5] = [
Expand Down
6 changes: 6 additions & 0 deletions codex-rs/exec/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ pub struct Cli {
#[arg(long = "oss", default_value_t = false)]
pub oss: bool,

/// Select the BrowserOS model provider using a config file.
/// Loads provider configuration (base_url, api_key, model_name, MCP servers, etc.)
/// from the specified TOML file. Similar to --oss but uses a custom config file.
#[arg(long = "browseros", value_name = "CONFIG_FILE", value_hint = clap::ValueHint::FilePath)]
pub browseros_config: Option<PathBuf>,

/// Select the sandbox policy to use when executing model-generated shell
/// commands.
#[arg(long = "sandbox", short = 's', value_enum)]
Expand Down
7 changes: 6 additions & 1 deletion codex-rs/exec/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
images,
model: model_cli_arg,
oss,
browseros_config,
config_profile,
full_auto,
dangerously_bypass_approvals_and_sandbox,
Expand Down Expand Up @@ -149,6 +150,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
// When using `--oss`, let the bootstrapper pick the model (defaulting to
// gpt-oss:20b) and ensure it is present locally. Also, force the built‑in
// `oss` model provider.
// When using `--browseros`, BrowserOS config takes precedence over model and model_provider.
let model = if let Some(model) = model_cli_arg {
Some(model)
} else if oss {
Expand All @@ -157,7 +159,9 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
None // No model specified, will use the default.
};

let model_provider = if oss {
let model_provider = if browseros_config.is_some() {
Some(codex_core::BUILT_IN_BROWSEROS_MODEL_PROVIDER_ID.to_string())
} else if oss {
Some(BUILT_IN_OSS_MODEL_PROVIDER_ID.to_string())
} else {
None // No specific model provider override.
Expand All @@ -181,6 +185,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
tools_web_search_request: None,
experimental_sandbox_command_assessment: None,
additional_writable_roots: Vec::new(),
browseros_config,
};
// Parse `-c` overrides.
let cli_kv_overrides = match config_overrides.parse_overrides() {
Expand Down
1 change: 1 addition & 0 deletions codex-rs/mcp-server/src/codex_tool_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ impl CodexToolCallParam {
tools_web_search_request: None,
experimental_sandbox_command_assessment: None,
additional_writable_roots: Vec::new(),
browseros_config: None,
};

let cli_overrides = cli_overrides
Expand Down
6 changes: 6 additions & 0 deletions codex-rs/tui/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ pub struct Cli {
#[arg(long = "oss", default_value_t = false)]
pub oss: bool,

/// Select the BrowserOS model provider using a config file.
/// Loads provider configuration (base_url, api_key, model_name, MCP servers, etc.)
/// from the specified TOML file. Similar to --oss but uses a custom config file.
#[arg(long = "browseros", value_name = "CONFIG_FILE", value_hint = clap::ValueHint::FilePath)]
pub browseros_config: Option<PathBuf>,

/// Configuration profile from config.toml to specify default options.
#[arg(long = "profile", short = 'p')]
pub config_profile: Option<String>,
Expand Down
Loading
Loading