feat: feature flag (#4948)
Add proper feature flag instead of having custom flags for everything. This is just for experimental/wip part of the code It can be used through CLI: ```bash codex --enable unified_exec --disable view_image_tool ``` Or in the `config.toml` ```toml # Global toggles applied to every profile unless overridden. [features] apply_patch_freeform = true view_image_tool = false ``` Follow-up: In a following PR, the goal is to have a default have `bundles` of features that we can associate to a model
This commit is contained in:
@@ -26,6 +26,8 @@ use supports_color::Stream;
|
|||||||
mod mcp_cmd;
|
mod mcp_cmd;
|
||||||
|
|
||||||
use crate::mcp_cmd::McpCli;
|
use crate::mcp_cmd::McpCli;
|
||||||
|
use codex_core::config::Config;
|
||||||
|
use codex_core::config::ConfigOverrides;
|
||||||
|
|
||||||
/// Codex CLI
|
/// Codex CLI
|
||||||
///
|
///
|
||||||
@@ -45,6 +47,9 @@ struct MultitoolCli {
|
|||||||
#[clap(flatten)]
|
#[clap(flatten)]
|
||||||
pub config_overrides: CliConfigOverrides,
|
pub config_overrides: CliConfigOverrides,
|
||||||
|
|
||||||
|
#[clap(flatten)]
|
||||||
|
pub feature_toggles: FeatureToggles,
|
||||||
|
|
||||||
#[clap(flatten)]
|
#[clap(flatten)]
|
||||||
interactive: TuiCli,
|
interactive: TuiCli,
|
||||||
|
|
||||||
@@ -97,6 +102,9 @@ enum Subcommand {
|
|||||||
/// Internal: run the responses API proxy.
|
/// Internal: run the responses API proxy.
|
||||||
#[clap(hide = true)]
|
#[clap(hide = true)]
|
||||||
ResponsesApiProxy(ResponsesApiProxyArgs),
|
ResponsesApiProxy(ResponsesApiProxyArgs),
|
||||||
|
|
||||||
|
/// Inspect feature flags.
|
||||||
|
Features(FeaturesCli),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Parser)]
|
#[derive(Debug, Parser)]
|
||||||
@@ -231,6 +239,53 @@ fn print_exit_messages(exit_info: AppExitInfo) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Parser, Clone)]
|
||||||
|
struct FeatureToggles {
|
||||||
|
/// Enable a feature (repeatable). Equivalent to `-c features.<name>=true`.
|
||||||
|
#[arg(long = "enable", value_name = "FEATURE", action = clap::ArgAction::Append, global = true)]
|
||||||
|
enable: Vec<String>,
|
||||||
|
|
||||||
|
/// Disable a feature (repeatable). Equivalent to `-c features.<name>=false`.
|
||||||
|
#[arg(long = "disable", value_name = "FEATURE", action = clap::ArgAction::Append, global = true)]
|
||||||
|
disable: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FeatureToggles {
|
||||||
|
fn to_overrides(&self) -> Vec<String> {
|
||||||
|
let mut v = Vec::new();
|
||||||
|
for k in &self.enable {
|
||||||
|
v.push(format!("features.{k}=true"));
|
||||||
|
}
|
||||||
|
for k in &self.disable {
|
||||||
|
v.push(format!("features.{k}=false"));
|
||||||
|
}
|
||||||
|
v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
struct FeaturesCli {
|
||||||
|
#[command(subcommand)]
|
||||||
|
sub: FeaturesSubcommand,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
enum FeaturesSubcommand {
|
||||||
|
/// List known features with their stage and effective state.
|
||||||
|
List,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stage_str(stage: codex_core::features::Stage) -> &'static str {
|
||||||
|
use codex_core::features::Stage;
|
||||||
|
match stage {
|
||||||
|
Stage::Experimental => "experimental",
|
||||||
|
Stage::Beta => "beta",
|
||||||
|
Stage::Stable => "stable",
|
||||||
|
Stage::Deprecated => "deprecated",
|
||||||
|
Stage::Removed => "removed",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// As early as possible in the process lifecycle, apply hardening measures. We
|
/// As early as possible in the process lifecycle, apply hardening measures. We
|
||||||
/// skip this in debug builds to avoid interfering with debugging.
|
/// skip this in debug builds to avoid interfering with debugging.
|
||||||
#[ctor::ctor]
|
#[ctor::ctor]
|
||||||
@@ -248,11 +303,17 @@ fn main() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
async fn cli_main(codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()> {
|
async fn cli_main(codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()> {
|
||||||
let MultitoolCli {
|
let MultitoolCli {
|
||||||
config_overrides: root_config_overrides,
|
config_overrides: mut root_config_overrides,
|
||||||
|
feature_toggles,
|
||||||
mut interactive,
|
mut interactive,
|
||||||
subcommand,
|
subcommand,
|
||||||
} = MultitoolCli::parse();
|
} = MultitoolCli::parse();
|
||||||
|
|
||||||
|
// Fold --enable/--disable into config overrides so they flow to all subcommands.
|
||||||
|
root_config_overrides
|
||||||
|
.raw_overrides
|
||||||
|
.extend(feature_toggles.to_overrides());
|
||||||
|
|
||||||
match subcommand {
|
match subcommand {
|
||||||
None => {
|
None => {
|
||||||
prepend_config_flags(
|
prepend_config_flags(
|
||||||
@@ -381,6 +442,30 @@ async fn cli_main(codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()
|
|||||||
Some(Subcommand::GenerateTs(gen_cli)) => {
|
Some(Subcommand::GenerateTs(gen_cli)) => {
|
||||||
codex_protocol_ts::generate_ts(&gen_cli.out_dir, gen_cli.prettier.as_deref())?;
|
codex_protocol_ts::generate_ts(&gen_cli.out_dir, gen_cli.prettier.as_deref())?;
|
||||||
}
|
}
|
||||||
|
Some(Subcommand::Features(FeaturesCli { sub })) => match sub {
|
||||||
|
FeaturesSubcommand::List => {
|
||||||
|
// Respect root-level `-c` overrides plus top-level flags like `--profile`.
|
||||||
|
let cli_kv_overrides = root_config_overrides
|
||||||
|
.parse_overrides()
|
||||||
|
.map_err(|e| anyhow::anyhow!(e))?;
|
||||||
|
|
||||||
|
// Thread through relevant top-level flags (at minimum, `--profile`).
|
||||||
|
// Also honor `--search` since it maps to a feature toggle.
|
||||||
|
let overrides = ConfigOverrides {
|
||||||
|
config_profile: interactive.config_profile.clone(),
|
||||||
|
tools_web_search_request: interactive.web_search.then_some(true),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let config = Config::load_with_cli_overrides(cli_kv_overrides, overrides).await?;
|
||||||
|
for def in codex_core::features::FEATURES.iter() {
|
||||||
|
let name = def.key;
|
||||||
|
let stage = stage_str(def.stage);
|
||||||
|
let enabled = config.features.enabled(def.id);
|
||||||
|
println!("{name}\t{stage}\t{enabled}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -484,6 +569,7 @@ mod tests {
|
|||||||
interactive,
|
interactive,
|
||||||
config_overrides: root_overrides,
|
config_overrides: root_overrides,
|
||||||
subcommand,
|
subcommand,
|
||||||
|
feature_toggles: _,
|
||||||
} = cli;
|
} = cli;
|
||||||
|
|
||||||
let Subcommand::Resume(ResumeCommand {
|
let Subcommand::Resume(ResumeCommand {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ use codex_core::config::load_global_mcp_servers;
|
|||||||
use codex_core::config::write_global_mcp_servers;
|
use codex_core::config::write_global_mcp_servers;
|
||||||
use codex_core::config_types::McpServerConfig;
|
use codex_core::config_types::McpServerConfig;
|
||||||
use codex_core::config_types::McpServerTransportConfig;
|
use codex_core::config_types::McpServerTransportConfig;
|
||||||
|
use codex_core::features::Feature;
|
||||||
use codex_core::mcp::auth::compute_auth_statuses;
|
use codex_core::mcp::auth::compute_auth_statuses;
|
||||||
use codex_core::protocol::McpAuthStatus;
|
use codex_core::protocol::McpAuthStatus;
|
||||||
use codex_rmcp_client::delete_oauth_tokens;
|
use codex_rmcp_client::delete_oauth_tokens;
|
||||||
@@ -285,7 +286,7 @@ async fn run_login(config_overrides: &CliConfigOverrides, login_args: LoginArgs)
|
|||||||
.await
|
.await
|
||||||
.context("failed to load configuration")?;
|
.context("failed to load configuration")?;
|
||||||
|
|
||||||
if !config.use_experimental_use_rmcp_client {
|
if !config.features.enabled(Feature::RmcpClient) {
|
||||||
bail!(
|
bail!(
|
||||||
"OAuth login is only supported when experimental_use_rmcp_client is true in config.toml."
|
"OAuth login is only supported when experimental_use_rmcp_client is true in config.toml."
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -365,7 +365,9 @@ impl Session {
|
|||||||
|
|
||||||
let mcp_fut = McpConnectionManager::new(
|
let mcp_fut = McpConnectionManager::new(
|
||||||
config.mcp_servers.clone(),
|
config.mcp_servers.clone(),
|
||||||
config.use_experimental_use_rmcp_client,
|
config
|
||||||
|
.features
|
||||||
|
.enabled(crate::features::Feature::RmcpClient),
|
||||||
config.mcp_oauth_credentials_store_mode,
|
config.mcp_oauth_credentials_store_mode,
|
||||||
);
|
);
|
||||||
let default_shell_fut = shell::default_user_shell();
|
let default_shell_fut = shell::default_user_shell();
|
||||||
@@ -447,12 +449,7 @@ impl Session {
|
|||||||
client,
|
client,
|
||||||
tools_config: ToolsConfig::new(&ToolsConfigParams {
|
tools_config: ToolsConfig::new(&ToolsConfigParams {
|
||||||
model_family: &config.model_family,
|
model_family: &config.model_family,
|
||||||
include_plan_tool: config.include_plan_tool,
|
features: &config.features,
|
||||||
include_apply_patch_tool: config.include_apply_patch_tool,
|
|
||||||
include_web_search_request: config.tools_web_search_request,
|
|
||||||
use_streamable_shell_tool: config.use_experimental_streamable_shell_tool,
|
|
||||||
include_view_image_tool: config.include_view_image_tool,
|
|
||||||
experimental_unified_exec_tool: config.use_experimental_unified_exec_tool,
|
|
||||||
}),
|
}),
|
||||||
user_instructions,
|
user_instructions,
|
||||||
base_instructions,
|
base_instructions,
|
||||||
@@ -1196,12 +1193,7 @@ async fn submission_loop(
|
|||||||
|
|
||||||
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
||||||
model_family: &effective_family,
|
model_family: &effective_family,
|
||||||
include_plan_tool: config.include_plan_tool,
|
features: &config.features,
|
||||||
include_apply_patch_tool: config.include_apply_patch_tool,
|
|
||||||
include_web_search_request: config.tools_web_search_request,
|
|
||||||
use_streamable_shell_tool: config.use_experimental_streamable_shell_tool,
|
|
||||||
include_view_image_tool: config.include_view_image_tool,
|
|
||||||
experimental_unified_exec_tool: config.use_experimental_unified_exec_tool,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let new_turn_context = TurnContext {
|
let new_turn_context = TurnContext {
|
||||||
@@ -1298,14 +1290,7 @@ async fn submission_loop(
|
|||||||
client,
|
client,
|
||||||
tools_config: ToolsConfig::new(&ToolsConfigParams {
|
tools_config: ToolsConfig::new(&ToolsConfigParams {
|
||||||
model_family: &model_family,
|
model_family: &model_family,
|
||||||
include_plan_tool: config.include_plan_tool,
|
features: &config.features,
|
||||||
include_apply_patch_tool: config.include_apply_patch_tool,
|
|
||||||
include_web_search_request: config.tools_web_search_request,
|
|
||||||
use_streamable_shell_tool: config
|
|
||||||
.use_experimental_streamable_shell_tool,
|
|
||||||
include_view_image_tool: config.include_view_image_tool,
|
|
||||||
experimental_unified_exec_tool: config
|
|
||||||
.use_experimental_unified_exec_tool,
|
|
||||||
}),
|
}),
|
||||||
user_instructions: turn_context.user_instructions.clone(),
|
user_instructions: turn_context.user_instructions.clone(),
|
||||||
base_instructions: turn_context.base_instructions.clone(),
|
base_instructions: turn_context.base_instructions.clone(),
|
||||||
@@ -1537,14 +1522,15 @@ async fn spawn_review_thread(
|
|||||||
let model = config.review_model.clone();
|
let model = config.review_model.clone();
|
||||||
let review_model_family = find_family_for_model(&model)
|
let review_model_family = find_family_for_model(&model)
|
||||||
.unwrap_or_else(|| parent_turn_context.client.get_model_family());
|
.unwrap_or_else(|| parent_turn_context.client.get_model_family());
|
||||||
|
// For reviews, disable plan, web_search, view_image regardless of global settings.
|
||||||
|
let mut review_features = config.features.clone();
|
||||||
|
review_features.disable(crate::features::Feature::PlanTool);
|
||||||
|
review_features.disable(crate::features::Feature::WebSearchRequest);
|
||||||
|
review_features.disable(crate::features::Feature::ViewImageTool);
|
||||||
|
review_features.disable(crate::features::Feature::StreamableShell);
|
||||||
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
||||||
model_family: &review_model_family,
|
model_family: &review_model_family,
|
||||||
include_plan_tool: false,
|
features: &review_features,
|
||||||
include_apply_patch_tool: config.include_apply_patch_tool,
|
|
||||||
include_web_search_request: false,
|
|
||||||
use_streamable_shell_tool: false,
|
|
||||||
include_view_image_tool: false,
|
|
||||||
experimental_unified_exec_tool: config.use_experimental_unified_exec_tool,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let base_instructions = REVIEW_PROMPT.to_string();
|
let base_instructions = REVIEW_PROMPT.to_string();
|
||||||
@@ -2758,12 +2744,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
||||||
model_family: &config.model_family,
|
model_family: &config.model_family,
|
||||||
include_plan_tool: config.include_plan_tool,
|
features: &config.features,
|
||||||
include_apply_patch_tool: config.include_apply_patch_tool,
|
|
||||||
include_web_search_request: config.tools_web_search_request,
|
|
||||||
use_streamable_shell_tool: config.use_experimental_streamable_shell_tool,
|
|
||||||
include_view_image_tool: config.include_view_image_tool,
|
|
||||||
experimental_unified_exec_tool: config.use_experimental_unified_exec_tool,
|
|
||||||
});
|
});
|
||||||
let turn_context = TurnContext {
|
let turn_context = TurnContext {
|
||||||
client,
|
client,
|
||||||
@@ -2831,12 +2812,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
||||||
model_family: &config.model_family,
|
model_family: &config.model_family,
|
||||||
include_plan_tool: config.include_plan_tool,
|
features: &config.features,
|
||||||
include_apply_patch_tool: config.include_apply_patch_tool,
|
|
||||||
include_web_search_request: config.tools_web_search_request,
|
|
||||||
use_streamable_shell_tool: config.use_experimental_streamable_shell_tool,
|
|
||||||
include_view_image_tool: config.include_view_image_tool,
|
|
||||||
experimental_unified_exec_tool: config.use_experimental_unified_exec_tool,
|
|
||||||
});
|
});
|
||||||
let turn_context = Arc::new(TurnContext {
|
let turn_context = Arc::new(TurnContext {
|
||||||
client,
|
client,
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ use crate::config_types::ShellEnvironmentPolicy;
|
|||||||
use crate::config_types::ShellEnvironmentPolicyToml;
|
use crate::config_types::ShellEnvironmentPolicyToml;
|
||||||
use crate::config_types::Tui;
|
use crate::config_types::Tui;
|
||||||
use crate::config_types::UriBasedFileOpener;
|
use crate::config_types::UriBasedFileOpener;
|
||||||
|
use crate::features::Feature;
|
||||||
|
use crate::features::FeatureOverrides;
|
||||||
|
use crate::features::Features;
|
||||||
|
use crate::features::FeaturesToml;
|
||||||
use crate::git_info::resolve_root_git_project_for_trust;
|
use crate::git_info::resolve_root_git_project_for_trust;
|
||||||
use crate::model_family::ModelFamily;
|
use crate::model_family::ModelFamily;
|
||||||
use crate::model_family::derive_default_model_family;
|
use crate::model_family::derive_default_model_family;
|
||||||
@@ -218,6 +222,9 @@ pub struct Config {
|
|||||||
/// Include the `view_image` tool that lets the agent attach a local image path to context.
|
/// Include the `view_image` tool that lets the agent attach a local image path to context.
|
||||||
pub include_view_image_tool: bool,
|
pub include_view_image_tool: bool,
|
||||||
|
|
||||||
|
/// Centralized feature flags; source of truth for feature gating.
|
||||||
|
pub features: Features,
|
||||||
|
|
||||||
/// The active profile name used to derive this `Config` (if any).
|
/// The active profile name used to derive this `Config` (if any).
|
||||||
pub active_profile: Option<String>,
|
pub active_profile: Option<String>,
|
||||||
|
|
||||||
@@ -794,19 +801,15 @@ pub struct ConfigToml {
|
|||||||
/// Base URL for requests to ChatGPT (as opposed to the OpenAI API).
|
/// Base URL for requests to ChatGPT (as opposed to the OpenAI API).
|
||||||
pub chatgpt_base_url: Option<String>,
|
pub chatgpt_base_url: Option<String>,
|
||||||
|
|
||||||
/// Experimental path to a file whose contents replace the built-in BASE_INSTRUCTIONS.
|
|
||||||
pub experimental_instructions_file: Option<PathBuf>,
|
|
||||||
|
|
||||||
pub experimental_use_exec_command_tool: Option<bool>,
|
|
||||||
pub experimental_use_unified_exec_tool: Option<bool>,
|
|
||||||
pub experimental_use_rmcp_client: Option<bool>,
|
|
||||||
pub experimental_use_freeform_apply_patch: Option<bool>,
|
|
||||||
|
|
||||||
pub projects: Option<HashMap<String, ProjectConfig>>,
|
pub projects: Option<HashMap<String, ProjectConfig>>,
|
||||||
|
|
||||||
/// Nested tools section for feature toggles
|
/// Nested tools section for feature toggles
|
||||||
pub tools: Option<ToolsToml>,
|
pub tools: Option<ToolsToml>,
|
||||||
|
|
||||||
|
/// Centralized feature flags (new). Prefer this over individual toggles.
|
||||||
|
#[serde(default)]
|
||||||
|
pub features: Option<FeaturesToml>,
|
||||||
|
|
||||||
/// When true, disables burst-paste detection for typed input entirely.
|
/// When true, disables burst-paste detection for typed input entirely.
|
||||||
/// All characters are inserted as they are received, and no buffering
|
/// All characters are inserted as they are received, and no buffering
|
||||||
/// or placeholder replacement will occur for fast keypress bursts.
|
/// or placeholder replacement will occur for fast keypress bursts.
|
||||||
@@ -817,6 +820,13 @@ pub struct ConfigToml {
|
|||||||
|
|
||||||
/// Tracks whether the Windows onboarding screen has been acknowledged.
|
/// Tracks whether the Windows onboarding screen has been acknowledged.
|
||||||
pub windows_wsl_setup_acknowledged: Option<bool>,
|
pub windows_wsl_setup_acknowledged: Option<bool>,
|
||||||
|
|
||||||
|
/// Legacy, now use features
|
||||||
|
pub experimental_instructions_file: Option<PathBuf>,
|
||||||
|
pub experimental_use_exec_command_tool: Option<bool>,
|
||||||
|
pub experimental_use_unified_exec_tool: Option<bool>,
|
||||||
|
pub experimental_use_rmcp_client: Option<bool>,
|
||||||
|
pub experimental_use_freeform_apply_patch: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ConfigToml> for UserSavedConfig {
|
impl From<ConfigToml> for UserSavedConfig {
|
||||||
@@ -980,9 +990,9 @@ impl Config {
|
|||||||
config_profile: config_profile_key,
|
config_profile: config_profile_key,
|
||||||
codex_linux_sandbox_exe,
|
codex_linux_sandbox_exe,
|
||||||
base_instructions,
|
base_instructions,
|
||||||
include_plan_tool,
|
include_plan_tool: include_plan_tool_override,
|
||||||
include_apply_patch_tool,
|
include_apply_patch_tool: include_apply_patch_tool_override,
|
||||||
include_view_image_tool,
|
include_view_image_tool: include_view_image_tool_override,
|
||||||
show_raw_agent_reasoning,
|
show_raw_agent_reasoning,
|
||||||
tools_web_search_request: override_tools_web_search_request,
|
tools_web_search_request: override_tools_web_search_request,
|
||||||
} = overrides;
|
} = overrides;
|
||||||
@@ -1005,6 +1015,15 @@ impl Config {
|
|||||||
None => ConfigProfile::default(),
|
None => ConfigProfile::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let feature_overrides = FeatureOverrides {
|
||||||
|
include_plan_tool: include_plan_tool_override,
|
||||||
|
include_apply_patch_tool: include_apply_patch_tool_override,
|
||||||
|
include_view_image_tool: include_view_image_tool_override,
|
||||||
|
web_search_request: override_tools_web_search_request,
|
||||||
|
};
|
||||||
|
|
||||||
|
let features = Features::from_config(&cfg, &config_profile, feature_overrides);
|
||||||
|
|
||||||
let sandbox_policy = cfg.derive_sandbox_policy(sandbox_mode);
|
let sandbox_policy = cfg.derive_sandbox_policy(sandbox_mode);
|
||||||
|
|
||||||
let mut model_providers = built_in_model_providers();
|
let mut model_providers = built_in_model_providers();
|
||||||
@@ -1050,13 +1069,13 @@ impl Config {
|
|||||||
|
|
||||||
let history = cfg.history.unwrap_or_default();
|
let history = cfg.history.unwrap_or_default();
|
||||||
|
|
||||||
let tools_web_search_request = override_tools_web_search_request
|
let include_plan_tool_flag = features.enabled(Feature::PlanTool);
|
||||||
.or(cfg.tools.as_ref().and_then(|t| t.web_search))
|
let include_apply_patch_tool_flag = features.enabled(Feature::ApplyPatchFreeform);
|
||||||
.unwrap_or(false);
|
let include_view_image_tool_flag = features.enabled(Feature::ViewImageTool);
|
||||||
|
let tools_web_search_request = features.enabled(Feature::WebSearchRequest);
|
||||||
let include_view_image_tool = include_view_image_tool
|
let use_experimental_streamable_shell_tool = features.enabled(Feature::StreamableShell);
|
||||||
.or(cfg.tools.as_ref().and_then(|t| t.view_image))
|
let use_experimental_unified_exec_tool = features.enabled(Feature::UnifiedExec);
|
||||||
.unwrap_or(true);
|
let use_experimental_use_rmcp_client = features.enabled(Feature::RmcpClient);
|
||||||
|
|
||||||
let model = model
|
let model = model
|
||||||
.or(config_profile.model)
|
.or(config_profile.model)
|
||||||
@@ -1164,19 +1183,14 @@ impl Config {
|
|||||||
.chatgpt_base_url
|
.chatgpt_base_url
|
||||||
.or(cfg.chatgpt_base_url)
|
.or(cfg.chatgpt_base_url)
|
||||||
.unwrap_or("https://chatgpt.com/backend-api/".to_string()),
|
.unwrap_or("https://chatgpt.com/backend-api/".to_string()),
|
||||||
include_plan_tool: include_plan_tool.unwrap_or(false),
|
include_plan_tool: include_plan_tool_flag,
|
||||||
include_apply_patch_tool: include_apply_patch_tool
|
include_apply_patch_tool: include_apply_patch_tool_flag,
|
||||||
.or(cfg.experimental_use_freeform_apply_patch)
|
|
||||||
.unwrap_or(false),
|
|
||||||
tools_web_search_request,
|
tools_web_search_request,
|
||||||
use_experimental_streamable_shell_tool: cfg
|
use_experimental_streamable_shell_tool,
|
||||||
.experimental_use_exec_command_tool
|
use_experimental_unified_exec_tool,
|
||||||
.unwrap_or(false),
|
use_experimental_use_rmcp_client,
|
||||||
use_experimental_unified_exec_tool: cfg
|
include_view_image_tool: include_view_image_tool_flag,
|
||||||
.experimental_use_unified_exec_tool
|
features,
|
||||||
.unwrap_or(false),
|
|
||||||
use_experimental_use_rmcp_client: cfg.experimental_use_rmcp_client.unwrap_or(false),
|
|
||||||
include_view_image_tool,
|
|
||||||
active_profile: active_profile_name,
|
active_profile: active_profile_name,
|
||||||
windows_wsl_setup_acknowledged: cfg.windows_wsl_setup_acknowledged.unwrap_or(false),
|
windows_wsl_setup_acknowledged: cfg.windows_wsl_setup_acknowledged.unwrap_or(false),
|
||||||
disable_paste_burst: cfg.disable_paste_burst.unwrap_or(false),
|
disable_paste_burst: cfg.disable_paste_burst.unwrap_or(false),
|
||||||
@@ -1309,6 +1323,7 @@ pub fn log_dir(cfg: &Config) -> std::io::Result<PathBuf> {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use crate::config_types::HistoryPersistence;
|
use crate::config_types::HistoryPersistence;
|
||||||
use crate::config_types::Notifications;
|
use crate::config_types::Notifications;
|
||||||
|
use crate::features::Feature;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
@@ -1436,6 +1451,93 @@ exclude_slash_tmp = true
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn profile_legacy_toggles_override_base() -> std::io::Result<()> {
|
||||||
|
let codex_home = TempDir::new()?;
|
||||||
|
let mut profiles = HashMap::new();
|
||||||
|
profiles.insert(
|
||||||
|
"work".to_string(),
|
||||||
|
ConfigProfile {
|
||||||
|
include_plan_tool: Some(true),
|
||||||
|
include_view_image_tool: Some(false),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let cfg = ConfigToml {
|
||||||
|
profiles,
|
||||||
|
profile: Some("work".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let config = Config::load_from_base_config_with_overrides(
|
||||||
|
cfg,
|
||||||
|
ConfigOverrides::default(),
|
||||||
|
codex_home.path().to_path_buf(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
assert!(config.features.enabled(Feature::PlanTool));
|
||||||
|
assert!(!config.features.enabled(Feature::ViewImageTool));
|
||||||
|
assert!(config.include_plan_tool);
|
||||||
|
assert!(!config.include_view_image_tool);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn feature_table_overrides_legacy_flags() -> std::io::Result<()> {
|
||||||
|
let codex_home = TempDir::new()?;
|
||||||
|
let mut entries = BTreeMap::new();
|
||||||
|
entries.insert("plan_tool".to_string(), false);
|
||||||
|
entries.insert("apply_patch_freeform".to_string(), false);
|
||||||
|
let cfg = ConfigToml {
|
||||||
|
features: Some(crate::features::FeaturesToml { entries }),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let config = Config::load_from_base_config_with_overrides(
|
||||||
|
cfg,
|
||||||
|
ConfigOverrides::default(),
|
||||||
|
codex_home.path().to_path_buf(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
assert!(!config.features.enabled(Feature::PlanTool));
|
||||||
|
assert!(!config.features.enabled(Feature::ApplyPatchFreeform));
|
||||||
|
assert!(!config.include_plan_tool);
|
||||||
|
assert!(!config.include_apply_patch_tool);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn legacy_toggles_map_to_features() -> std::io::Result<()> {
|
||||||
|
let codex_home = TempDir::new()?;
|
||||||
|
let cfg = ConfigToml {
|
||||||
|
experimental_use_exec_command_tool: Some(true),
|
||||||
|
experimental_use_unified_exec_tool: Some(true),
|
||||||
|
experimental_use_rmcp_client: Some(true),
|
||||||
|
experimental_use_freeform_apply_patch: Some(true),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let config = Config::load_from_base_config_with_overrides(
|
||||||
|
cfg,
|
||||||
|
ConfigOverrides::default(),
|
||||||
|
codex_home.path().to_path_buf(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
assert!(config.features.enabled(Feature::ApplyPatchFreeform));
|
||||||
|
assert!(config.features.enabled(Feature::StreamableShell));
|
||||||
|
assert!(config.features.enabled(Feature::UnifiedExec));
|
||||||
|
assert!(config.features.enabled(Feature::RmcpClient));
|
||||||
|
|
||||||
|
assert!(config.include_apply_patch_tool);
|
||||||
|
assert!(config.use_experimental_streamable_shell_tool);
|
||||||
|
assert!(config.use_experimental_unified_exec_tool);
|
||||||
|
assert!(config.use_experimental_use_rmcp_client);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn config_honors_explicit_file_oauth_store_mode() -> std::io::Result<()> {
|
fn config_honors_explicit_file_oauth_store_mode() -> std::io::Result<()> {
|
||||||
let codex_home = TempDir::new()?;
|
let codex_home = TempDir::new()?;
|
||||||
@@ -2120,6 +2222,7 @@ model_verbosity = "high"
|
|||||||
use_experimental_unified_exec_tool: false,
|
use_experimental_unified_exec_tool: false,
|
||||||
use_experimental_use_rmcp_client: false,
|
use_experimental_use_rmcp_client: false,
|
||||||
include_view_image_tool: true,
|
include_view_image_tool: true,
|
||||||
|
features: Features::with_defaults(),
|
||||||
active_profile: Some("o3".to_string()),
|
active_profile: Some("o3".to_string()),
|
||||||
windows_wsl_setup_acknowledged: false,
|
windows_wsl_setup_acknowledged: false,
|
||||||
disable_paste_burst: false,
|
disable_paste_burst: false,
|
||||||
@@ -2183,6 +2286,7 @@ model_verbosity = "high"
|
|||||||
use_experimental_unified_exec_tool: false,
|
use_experimental_unified_exec_tool: false,
|
||||||
use_experimental_use_rmcp_client: false,
|
use_experimental_use_rmcp_client: false,
|
||||||
include_view_image_tool: true,
|
include_view_image_tool: true,
|
||||||
|
features: Features::with_defaults(),
|
||||||
active_profile: Some("gpt3".to_string()),
|
active_profile: Some("gpt3".to_string()),
|
||||||
windows_wsl_setup_acknowledged: false,
|
windows_wsl_setup_acknowledged: false,
|
||||||
disable_paste_burst: false,
|
disable_paste_burst: false,
|
||||||
@@ -2261,6 +2365,7 @@ model_verbosity = "high"
|
|||||||
use_experimental_unified_exec_tool: false,
|
use_experimental_unified_exec_tool: false,
|
||||||
use_experimental_use_rmcp_client: false,
|
use_experimental_use_rmcp_client: false,
|
||||||
include_view_image_tool: true,
|
include_view_image_tool: true,
|
||||||
|
features: Features::with_defaults(),
|
||||||
active_profile: Some("zdr".to_string()),
|
active_profile: Some("zdr".to_string()),
|
||||||
windows_wsl_setup_acknowledged: false,
|
windows_wsl_setup_acknowledged: false,
|
||||||
disable_paste_burst: false,
|
disable_paste_burst: false,
|
||||||
@@ -2325,6 +2430,7 @@ model_verbosity = "high"
|
|||||||
use_experimental_unified_exec_tool: false,
|
use_experimental_unified_exec_tool: false,
|
||||||
use_experimental_use_rmcp_client: false,
|
use_experimental_use_rmcp_client: false,
|
||||||
include_view_image_tool: true,
|
include_view_image_tool: true,
|
||||||
|
features: Features::with_defaults(),
|
||||||
active_profile: Some("gpt5".to_string()),
|
active_profile: Some("gpt5".to_string()),
|
||||||
windows_wsl_setup_acknowledged: false,
|
windows_wsl_setup_acknowledged: false,
|
||||||
disable_paste_burst: false,
|
disable_paste_burst: false,
|
||||||
|
|||||||
@@ -20,6 +20,18 @@ pub struct ConfigProfile {
|
|||||||
pub model_verbosity: Option<Verbosity>,
|
pub model_verbosity: Option<Verbosity>,
|
||||||
pub chatgpt_base_url: Option<String>,
|
pub chatgpt_base_url: Option<String>,
|
||||||
pub experimental_instructions_file: Option<PathBuf>,
|
pub experimental_instructions_file: Option<PathBuf>,
|
||||||
|
pub include_plan_tool: Option<bool>,
|
||||||
|
pub include_apply_patch_tool: Option<bool>,
|
||||||
|
pub include_view_image_tool: Option<bool>,
|
||||||
|
pub experimental_use_unified_exec_tool: Option<bool>,
|
||||||
|
pub experimental_use_exec_command_tool: Option<bool>,
|
||||||
|
pub experimental_use_rmcp_client: Option<bool>,
|
||||||
|
pub experimental_use_freeform_apply_patch: Option<bool>,
|
||||||
|
pub tools_web_search: Option<bool>,
|
||||||
|
pub tools_view_image: Option<bool>,
|
||||||
|
/// Optional feature toggles scoped to this profile.
|
||||||
|
#[serde(default)]
|
||||||
|
pub features: Option<crate::features::FeaturesToml>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ConfigProfile> for codex_app_server_protocol::Profile {
|
impl From<ConfigProfile> for codex_app_server_protocol::Profile {
|
||||||
|
|||||||
250
codex-rs/core/src/features.rs
Normal file
250
codex-rs/core/src/features.rs
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
//! Centralized feature flags and metadata.
|
||||||
|
//!
|
||||||
|
//! This module defines a small set of toggles that gate experimental and
|
||||||
|
//! optional behavior across the codebase. Instead of wiring individual
|
||||||
|
//! booleans through multiple types, call sites consult a single `Features`
|
||||||
|
//! container attached to `Config`.
|
||||||
|
|
||||||
|
use crate::config::ConfigToml;
|
||||||
|
use crate::config_profile::ConfigProfile;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::collections::BTreeSet;
|
||||||
|
|
||||||
|
mod legacy;
|
||||||
|
pub(crate) use legacy::LegacyFeatureToggles;
|
||||||
|
|
||||||
|
/// High-level lifecycle stage for a feature.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Stage {
|
||||||
|
Experimental,
|
||||||
|
Beta,
|
||||||
|
Stable,
|
||||||
|
Deprecated,
|
||||||
|
Removed,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unique features toggled via configuration.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub enum Feature {
|
||||||
|
/// Use the single unified PTY-backed exec tool.
|
||||||
|
UnifiedExec,
|
||||||
|
/// Use the streamable exec-command/write-stdin tool pair.
|
||||||
|
StreamableShell,
|
||||||
|
/// Use the official Rust MCP client (rmcp).
|
||||||
|
RmcpClient,
|
||||||
|
/// Include the plan tool.
|
||||||
|
PlanTool,
|
||||||
|
/// Include the freeform apply_patch tool.
|
||||||
|
ApplyPatchFreeform,
|
||||||
|
/// Include the view_image tool.
|
||||||
|
ViewImageTool,
|
||||||
|
/// Allow the model to request web searches.
|
||||||
|
WebSearchRequest,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Feature {
|
||||||
|
pub fn key(self) -> &'static str {
|
||||||
|
self.info().key
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stage(self) -> Stage {
|
||||||
|
self.info().stage
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default_enabled(self) -> bool {
|
||||||
|
self.info().default_enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
fn info(self) -> &'static FeatureSpec {
|
||||||
|
FEATURES
|
||||||
|
.iter()
|
||||||
|
.find(|spec| spec.id == self)
|
||||||
|
.unwrap_or_else(|| unreachable!("missing FeatureSpec for {:?}", self))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Holds the effective set of enabled features.
|
||||||
|
#[derive(Debug, Clone, Default, PartialEq)]
|
||||||
|
pub struct Features {
|
||||||
|
enabled: BTreeSet<Feature>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct FeatureOverrides {
|
||||||
|
pub include_plan_tool: Option<bool>,
|
||||||
|
pub include_apply_patch_tool: Option<bool>,
|
||||||
|
pub include_view_image_tool: Option<bool>,
|
||||||
|
pub web_search_request: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FeatureOverrides {
|
||||||
|
fn apply(self, features: &mut Features) {
|
||||||
|
LegacyFeatureToggles {
|
||||||
|
include_plan_tool: self.include_plan_tool,
|
||||||
|
include_apply_patch_tool: self.include_apply_patch_tool,
|
||||||
|
include_view_image_tool: self.include_view_image_tool,
|
||||||
|
tools_web_search: self.web_search_request,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.apply(features);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Features {
|
||||||
|
/// Starts with built-in defaults.
|
||||||
|
pub fn with_defaults() -> Self {
|
||||||
|
let mut set = BTreeSet::new();
|
||||||
|
for spec in FEATURES {
|
||||||
|
if spec.default_enabled {
|
||||||
|
set.insert(spec.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self { enabled: set }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn enabled(&self, f: Feature) -> bool {
|
||||||
|
self.enabled.contains(&f)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn enable(&mut self, f: Feature) {
|
||||||
|
self.enabled.insert(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn disable(&mut self, f: Feature) {
|
||||||
|
self.enabled.remove(&f);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply a table of key -> bool toggles (e.g. from TOML).
|
||||||
|
pub fn apply_map(&mut self, m: &BTreeMap<String, bool>) {
|
||||||
|
for (k, v) in m {
|
||||||
|
match feature_for_key(k) {
|
||||||
|
Some(feat) => {
|
||||||
|
if *v {
|
||||||
|
self.enable(feat);
|
||||||
|
} else {
|
||||||
|
self.disable(feat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
tracing::warn!("unknown feature key in config: {k}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_config(
|
||||||
|
cfg: &ConfigToml,
|
||||||
|
config_profile: &ConfigProfile,
|
||||||
|
overrides: FeatureOverrides,
|
||||||
|
) -> Self {
|
||||||
|
let mut features = Features::with_defaults();
|
||||||
|
|
||||||
|
let base_legacy = LegacyFeatureToggles {
|
||||||
|
experimental_use_freeform_apply_patch: cfg.experimental_use_freeform_apply_patch,
|
||||||
|
experimental_use_exec_command_tool: cfg.experimental_use_exec_command_tool,
|
||||||
|
experimental_use_unified_exec_tool: cfg.experimental_use_unified_exec_tool,
|
||||||
|
experimental_use_rmcp_client: cfg.experimental_use_rmcp_client,
|
||||||
|
tools_web_search: cfg.tools.as_ref().and_then(|t| t.web_search),
|
||||||
|
tools_view_image: cfg.tools.as_ref().and_then(|t| t.view_image),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
base_legacy.apply(&mut features);
|
||||||
|
|
||||||
|
if let Some(base_features) = cfg.features.as_ref() {
|
||||||
|
features.apply_map(&base_features.entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
let profile_legacy = LegacyFeatureToggles {
|
||||||
|
include_plan_tool: config_profile.include_plan_tool,
|
||||||
|
include_apply_patch_tool: config_profile.include_apply_patch_tool,
|
||||||
|
include_view_image_tool: config_profile.include_view_image_tool,
|
||||||
|
experimental_use_freeform_apply_patch: config_profile
|
||||||
|
.experimental_use_freeform_apply_patch,
|
||||||
|
experimental_use_exec_command_tool: config_profile.experimental_use_exec_command_tool,
|
||||||
|
experimental_use_unified_exec_tool: config_profile.experimental_use_unified_exec_tool,
|
||||||
|
experimental_use_rmcp_client: config_profile.experimental_use_rmcp_client,
|
||||||
|
tools_web_search: config_profile.tools_web_search,
|
||||||
|
tools_view_image: config_profile.tools_view_image,
|
||||||
|
};
|
||||||
|
profile_legacy.apply(&mut features);
|
||||||
|
if let Some(profile_features) = config_profile.features.as_ref() {
|
||||||
|
features.apply_map(&profile_features.entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
overrides.apply(&mut features);
|
||||||
|
|
||||||
|
features
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Keys accepted in `[features]` tables.
|
||||||
|
fn feature_for_key(key: &str) -> Option<Feature> {
|
||||||
|
for spec in FEATURES {
|
||||||
|
if spec.key == key {
|
||||||
|
return Some(spec.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
legacy::feature_for_key(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deserializable features table for TOML.
|
||||||
|
#[derive(Deserialize, Debug, Clone, Default, PartialEq)]
|
||||||
|
pub struct FeaturesToml {
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub entries: BTreeMap<String, bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Single, easy-to-read registry of all feature definitions.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct FeatureSpec {
|
||||||
|
pub id: Feature,
|
||||||
|
pub key: &'static str,
|
||||||
|
pub stage: Stage,
|
||||||
|
pub default_enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const FEATURES: &[FeatureSpec] = &[
|
||||||
|
FeatureSpec {
|
||||||
|
id: Feature::UnifiedExec,
|
||||||
|
key: "unified_exec",
|
||||||
|
stage: Stage::Experimental,
|
||||||
|
default_enabled: false,
|
||||||
|
},
|
||||||
|
FeatureSpec {
|
||||||
|
id: Feature::StreamableShell,
|
||||||
|
key: "streamable_shell",
|
||||||
|
stage: Stage::Experimental,
|
||||||
|
default_enabled: false,
|
||||||
|
},
|
||||||
|
FeatureSpec {
|
||||||
|
id: Feature::RmcpClient,
|
||||||
|
key: "rmcp_client",
|
||||||
|
stage: Stage::Experimental,
|
||||||
|
default_enabled: false,
|
||||||
|
},
|
||||||
|
FeatureSpec {
|
||||||
|
id: Feature::PlanTool,
|
||||||
|
key: "plan_tool",
|
||||||
|
stage: Stage::Stable,
|
||||||
|
default_enabled: false,
|
||||||
|
},
|
||||||
|
FeatureSpec {
|
||||||
|
id: Feature::ApplyPatchFreeform,
|
||||||
|
key: "apply_patch_freeform",
|
||||||
|
stage: Stage::Beta,
|
||||||
|
default_enabled: false,
|
||||||
|
},
|
||||||
|
FeatureSpec {
|
||||||
|
id: Feature::ViewImageTool,
|
||||||
|
key: "view_image_tool",
|
||||||
|
stage: Stage::Stable,
|
||||||
|
default_enabled: true,
|
||||||
|
},
|
||||||
|
FeatureSpec {
|
||||||
|
id: Feature::WebSearchRequest,
|
||||||
|
key: "web_search_request",
|
||||||
|
stage: Stage::Stable,
|
||||||
|
default_enabled: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
158
codex-rs/core/src/features/legacy.rs
Normal file
158
codex-rs/core/src/features/legacy.rs
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
use super::Feature;
|
||||||
|
use super::Features;
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
struct Alias {
|
||||||
|
legacy_key: &'static str,
|
||||||
|
feature: Feature,
|
||||||
|
}
|
||||||
|
|
||||||
|
const ALIASES: &[Alias] = &[
|
||||||
|
Alias {
|
||||||
|
legacy_key: "experimental_use_unified_exec_tool",
|
||||||
|
feature: Feature::UnifiedExec,
|
||||||
|
},
|
||||||
|
Alias {
|
||||||
|
legacy_key: "experimental_use_exec_command_tool",
|
||||||
|
feature: Feature::StreamableShell,
|
||||||
|
},
|
||||||
|
Alias {
|
||||||
|
legacy_key: "experimental_use_rmcp_client",
|
||||||
|
feature: Feature::RmcpClient,
|
||||||
|
},
|
||||||
|
Alias {
|
||||||
|
legacy_key: "experimental_use_freeform_apply_patch",
|
||||||
|
feature: Feature::ApplyPatchFreeform,
|
||||||
|
},
|
||||||
|
Alias {
|
||||||
|
legacy_key: "include_apply_patch_tool",
|
||||||
|
feature: Feature::ApplyPatchFreeform,
|
||||||
|
},
|
||||||
|
Alias {
|
||||||
|
legacy_key: "include_plan_tool",
|
||||||
|
feature: Feature::PlanTool,
|
||||||
|
},
|
||||||
|
Alias {
|
||||||
|
legacy_key: "include_view_image_tool",
|
||||||
|
feature: Feature::ViewImageTool,
|
||||||
|
},
|
||||||
|
Alias {
|
||||||
|
legacy_key: "web_search",
|
||||||
|
feature: Feature::WebSearchRequest,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
pub(crate) fn feature_for_key(key: &str) -> Option<Feature> {
|
||||||
|
ALIASES
|
||||||
|
.iter()
|
||||||
|
.find(|alias| alias.legacy_key == key)
|
||||||
|
.map(|alias| {
|
||||||
|
log_alias(alias.legacy_key, alias.feature);
|
||||||
|
alias.feature
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct LegacyFeatureToggles {
|
||||||
|
pub include_plan_tool: Option<bool>,
|
||||||
|
pub include_apply_patch_tool: Option<bool>,
|
||||||
|
pub include_view_image_tool: Option<bool>,
|
||||||
|
pub experimental_use_freeform_apply_patch: Option<bool>,
|
||||||
|
pub experimental_use_exec_command_tool: Option<bool>,
|
||||||
|
pub experimental_use_unified_exec_tool: Option<bool>,
|
||||||
|
pub experimental_use_rmcp_client: Option<bool>,
|
||||||
|
pub tools_web_search: Option<bool>,
|
||||||
|
pub tools_view_image: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LegacyFeatureToggles {
|
||||||
|
pub fn apply(self, features: &mut Features) {
|
||||||
|
set_if_some(
|
||||||
|
features,
|
||||||
|
Feature::PlanTool,
|
||||||
|
self.include_plan_tool,
|
||||||
|
"include_plan_tool",
|
||||||
|
);
|
||||||
|
set_if_some(
|
||||||
|
features,
|
||||||
|
Feature::ApplyPatchFreeform,
|
||||||
|
self.include_apply_patch_tool,
|
||||||
|
"include_apply_patch_tool",
|
||||||
|
);
|
||||||
|
set_if_some(
|
||||||
|
features,
|
||||||
|
Feature::ApplyPatchFreeform,
|
||||||
|
self.experimental_use_freeform_apply_patch,
|
||||||
|
"experimental_use_freeform_apply_patch",
|
||||||
|
);
|
||||||
|
set_if_some(
|
||||||
|
features,
|
||||||
|
Feature::StreamableShell,
|
||||||
|
self.experimental_use_exec_command_tool,
|
||||||
|
"experimental_use_exec_command_tool",
|
||||||
|
);
|
||||||
|
set_if_some(
|
||||||
|
features,
|
||||||
|
Feature::UnifiedExec,
|
||||||
|
self.experimental_use_unified_exec_tool,
|
||||||
|
"experimental_use_unified_exec_tool",
|
||||||
|
);
|
||||||
|
set_if_some(
|
||||||
|
features,
|
||||||
|
Feature::RmcpClient,
|
||||||
|
self.experimental_use_rmcp_client,
|
||||||
|
"experimental_use_rmcp_client",
|
||||||
|
);
|
||||||
|
set_if_some(
|
||||||
|
features,
|
||||||
|
Feature::WebSearchRequest,
|
||||||
|
self.tools_web_search,
|
||||||
|
"tools.web_search",
|
||||||
|
);
|
||||||
|
set_if_some(
|
||||||
|
features,
|
||||||
|
Feature::ViewImageTool,
|
||||||
|
self.include_view_image_tool,
|
||||||
|
"include_view_image_tool",
|
||||||
|
);
|
||||||
|
set_if_some(
|
||||||
|
features,
|
||||||
|
Feature::ViewImageTool,
|
||||||
|
self.tools_view_image,
|
||||||
|
"tools.view_image",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_if_some(
|
||||||
|
features: &mut Features,
|
||||||
|
feature: Feature,
|
||||||
|
maybe_value: Option<bool>,
|
||||||
|
alias_key: &'static str,
|
||||||
|
) {
|
||||||
|
if let Some(enabled) = maybe_value {
|
||||||
|
set_feature(features, feature, enabled);
|
||||||
|
log_alias(alias_key, feature);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_feature(features: &mut Features, feature: Feature, enabled: bool) {
|
||||||
|
if enabled {
|
||||||
|
features.enable(feature);
|
||||||
|
} else {
|
||||||
|
features.disable(feature);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn log_alias(alias: &str, feature: Feature) {
|
||||||
|
let canonical = feature.key();
|
||||||
|
if alias == canonical {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
info!(
|
||||||
|
%alias,
|
||||||
|
canonical,
|
||||||
|
"legacy feature toggle detected; prefer `[features].{canonical}`"
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -29,6 +29,7 @@ pub mod exec;
|
|||||||
mod exec_command;
|
mod exec_command;
|
||||||
pub mod exec_env;
|
pub mod exec_env;
|
||||||
pub mod executor;
|
pub mod executor;
|
||||||
|
pub mod features;
|
||||||
mod flags;
|
mod flags;
|
||||||
pub mod git_info;
|
pub mod git_info;
|
||||||
pub mod landlock;
|
pub mod landlock;
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
use crate::client_common::tools::ResponsesApiTool;
|
use crate::client_common::tools::ResponsesApiTool;
|
||||||
use crate::client_common::tools::ToolSpec;
|
use crate::client_common::tools::ToolSpec;
|
||||||
|
use crate::features::Feature;
|
||||||
|
use crate::features::Features;
|
||||||
use crate::model_family::ModelFamily;
|
use crate::model_family::ModelFamily;
|
||||||
use crate::tools::handlers::PLAN_TOOL;
|
use crate::tools::handlers::PLAN_TOOL;
|
||||||
use crate::tools::handlers::apply_patch::ApplyPatchToolType;
|
use crate::tools::handlers::apply_patch::ApplyPatchToolType;
|
||||||
@@ -33,26 +35,23 @@ pub(crate) struct ToolsConfig {
|
|||||||
|
|
||||||
pub(crate) struct ToolsConfigParams<'a> {
|
pub(crate) struct ToolsConfigParams<'a> {
|
||||||
pub(crate) model_family: &'a ModelFamily,
|
pub(crate) model_family: &'a ModelFamily,
|
||||||
pub(crate) include_plan_tool: bool,
|
pub(crate) features: &'a Features,
|
||||||
pub(crate) include_apply_patch_tool: bool,
|
|
||||||
pub(crate) include_web_search_request: bool,
|
|
||||||
pub(crate) use_streamable_shell_tool: bool,
|
|
||||||
pub(crate) include_view_image_tool: bool,
|
|
||||||
pub(crate) experimental_unified_exec_tool: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToolsConfig {
|
impl ToolsConfig {
|
||||||
pub fn new(params: &ToolsConfigParams) -> Self {
|
pub fn new(params: &ToolsConfigParams) -> Self {
|
||||||
let ToolsConfigParams {
|
let ToolsConfigParams {
|
||||||
model_family,
|
model_family,
|
||||||
include_plan_tool,
|
features,
|
||||||
include_apply_patch_tool,
|
|
||||||
include_web_search_request,
|
|
||||||
use_streamable_shell_tool,
|
|
||||||
include_view_image_tool,
|
|
||||||
experimental_unified_exec_tool,
|
|
||||||
} = params;
|
} = params;
|
||||||
let shell_type = if *use_streamable_shell_tool {
|
let use_streamable_shell_tool = features.enabled(Feature::StreamableShell);
|
||||||
|
let experimental_unified_exec_tool = features.enabled(Feature::UnifiedExec);
|
||||||
|
let include_plan_tool = features.enabled(Feature::PlanTool);
|
||||||
|
let include_apply_patch_tool = features.enabled(Feature::ApplyPatchFreeform);
|
||||||
|
let include_web_search_request = features.enabled(Feature::WebSearchRequest);
|
||||||
|
let include_view_image_tool = features.enabled(Feature::ViewImageTool);
|
||||||
|
|
||||||
|
let shell_type = if use_streamable_shell_tool {
|
||||||
ConfigShellToolType::Streamable
|
ConfigShellToolType::Streamable
|
||||||
} else if model_family.uses_local_shell_tool {
|
} else if model_family.uses_local_shell_tool {
|
||||||
ConfigShellToolType::Local
|
ConfigShellToolType::Local
|
||||||
@@ -64,7 +63,7 @@ impl ToolsConfig {
|
|||||||
Some(ApplyPatchToolType::Freeform) => Some(ApplyPatchToolType::Freeform),
|
Some(ApplyPatchToolType::Freeform) => Some(ApplyPatchToolType::Freeform),
|
||||||
Some(ApplyPatchToolType::Function) => Some(ApplyPatchToolType::Function),
|
Some(ApplyPatchToolType::Function) => Some(ApplyPatchToolType::Function),
|
||||||
None => {
|
None => {
|
||||||
if *include_apply_patch_tool {
|
if include_apply_patch_tool {
|
||||||
Some(ApplyPatchToolType::Freeform)
|
Some(ApplyPatchToolType::Freeform)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@@ -74,11 +73,11 @@ impl ToolsConfig {
|
|||||||
|
|
||||||
Self {
|
Self {
|
||||||
shell_type,
|
shell_type,
|
||||||
plan_tool: *include_plan_tool,
|
plan_tool: include_plan_tool,
|
||||||
apply_patch_tool_type,
|
apply_patch_tool_type,
|
||||||
web_search_request: *include_web_search_request,
|
web_search_request: include_web_search_request,
|
||||||
include_view_image_tool: *include_view_image_tool,
|
include_view_image_tool,
|
||||||
experimental_unified_exec_tool: *experimental_unified_exec_tool,
|
experimental_unified_exec_tool,
|
||||||
experimental_supported_tools: model_family.experimental_supported_tools.clone(),
|
experimental_supported_tools: model_family.experimental_supported_tools.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -906,14 +905,13 @@ mod tests {
|
|||||||
fn test_build_specs() {
|
fn test_build_specs() {
|
||||||
let model_family = find_family_for_model("codex-mini-latest")
|
let model_family = find_family_for_model("codex-mini-latest")
|
||||||
.expect("codex-mini-latest should be a valid model family");
|
.expect("codex-mini-latest should be a valid model family");
|
||||||
|
let mut features = Features::with_defaults();
|
||||||
|
features.enable(Feature::PlanTool);
|
||||||
|
features.enable(Feature::WebSearchRequest);
|
||||||
|
features.enable(Feature::UnifiedExec);
|
||||||
let config = ToolsConfig::new(&ToolsConfigParams {
|
let config = ToolsConfig::new(&ToolsConfigParams {
|
||||||
model_family: &model_family,
|
model_family: &model_family,
|
||||||
include_plan_tool: true,
|
features: &features,
|
||||||
include_apply_patch_tool: false,
|
|
||||||
include_web_search_request: true,
|
|
||||||
use_streamable_shell_tool: false,
|
|
||||||
include_view_image_tool: true,
|
|
||||||
experimental_unified_exec_tool: true,
|
|
||||||
});
|
});
|
||||||
let (tools, _) = build_specs(&config, Some(HashMap::new())).build();
|
let (tools, _) = build_specs(&config, Some(HashMap::new())).build();
|
||||||
|
|
||||||
@@ -926,14 +924,13 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_build_specs_default_shell() {
|
fn test_build_specs_default_shell() {
|
||||||
let model_family = find_family_for_model("o3").expect("o3 should be a valid model family");
|
let model_family = find_family_for_model("o3").expect("o3 should be a valid model family");
|
||||||
|
let mut features = Features::with_defaults();
|
||||||
|
features.enable(Feature::PlanTool);
|
||||||
|
features.enable(Feature::WebSearchRequest);
|
||||||
|
features.enable(Feature::UnifiedExec);
|
||||||
let config = ToolsConfig::new(&ToolsConfigParams {
|
let config = ToolsConfig::new(&ToolsConfigParams {
|
||||||
model_family: &model_family,
|
model_family: &model_family,
|
||||||
include_plan_tool: true,
|
features: &features,
|
||||||
include_apply_patch_tool: false,
|
|
||||||
include_web_search_request: true,
|
|
||||||
use_streamable_shell_tool: false,
|
|
||||||
include_view_image_tool: true,
|
|
||||||
experimental_unified_exec_tool: true,
|
|
||||||
});
|
});
|
||||||
let (tools, _) = build_specs(&config, Some(HashMap::new())).build();
|
let (tools, _) = build_specs(&config, Some(HashMap::new())).build();
|
||||||
|
|
||||||
@@ -948,14 +945,12 @@ mod tests {
|
|||||||
fn test_parallel_support_flags() {
|
fn test_parallel_support_flags() {
|
||||||
let model_family = find_family_for_model("gpt-5-codex")
|
let model_family = find_family_for_model("gpt-5-codex")
|
||||||
.expect("codex-mini-latest should be a valid model family");
|
.expect("codex-mini-latest should be a valid model family");
|
||||||
|
let mut features = Features::with_defaults();
|
||||||
|
features.disable(Feature::ViewImageTool);
|
||||||
|
features.enable(Feature::UnifiedExec);
|
||||||
let config = ToolsConfig::new(&ToolsConfigParams {
|
let config = ToolsConfig::new(&ToolsConfigParams {
|
||||||
model_family: &model_family,
|
model_family: &model_family,
|
||||||
include_plan_tool: false,
|
features: &features,
|
||||||
include_apply_patch_tool: false,
|
|
||||||
include_web_search_request: false,
|
|
||||||
use_streamable_shell_tool: false,
|
|
||||||
include_view_image_tool: false,
|
|
||||||
experimental_unified_exec_tool: true,
|
|
||||||
});
|
});
|
||||||
let (tools, _) = build_specs(&config, None).build();
|
let (tools, _) = build_specs(&config, None).build();
|
||||||
|
|
||||||
@@ -969,14 +964,11 @@ mod tests {
|
|||||||
fn test_test_model_family_includes_sync_tool() {
|
fn test_test_model_family_includes_sync_tool() {
|
||||||
let model_family = find_family_for_model("test-gpt-5-codex")
|
let model_family = find_family_for_model("test-gpt-5-codex")
|
||||||
.expect("test-gpt-5-codex should be a valid model family");
|
.expect("test-gpt-5-codex should be a valid model family");
|
||||||
|
let mut features = Features::with_defaults();
|
||||||
|
features.disable(Feature::ViewImageTool);
|
||||||
let config = ToolsConfig::new(&ToolsConfigParams {
|
let config = ToolsConfig::new(&ToolsConfigParams {
|
||||||
model_family: &model_family,
|
model_family: &model_family,
|
||||||
include_plan_tool: false,
|
features: &features,
|
||||||
include_apply_patch_tool: false,
|
|
||||||
include_web_search_request: false,
|
|
||||||
use_streamable_shell_tool: false,
|
|
||||||
include_view_image_tool: false,
|
|
||||||
experimental_unified_exec_tool: false,
|
|
||||||
});
|
});
|
||||||
let (tools, _) = build_specs(&config, None).build();
|
let (tools, _) = build_specs(&config, None).build();
|
||||||
|
|
||||||
@@ -1001,14 +993,12 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_build_specs_mcp_tools() {
|
fn test_build_specs_mcp_tools() {
|
||||||
let model_family = find_family_for_model("o3").expect("o3 should be a valid model family");
|
let model_family = find_family_for_model("o3").expect("o3 should be a valid model family");
|
||||||
|
let mut features = Features::with_defaults();
|
||||||
|
features.enable(Feature::UnifiedExec);
|
||||||
|
features.enable(Feature::WebSearchRequest);
|
||||||
let config = ToolsConfig::new(&ToolsConfigParams {
|
let config = ToolsConfig::new(&ToolsConfigParams {
|
||||||
model_family: &model_family,
|
model_family: &model_family,
|
||||||
include_plan_tool: false,
|
features: &features,
|
||||||
include_apply_patch_tool: false,
|
|
||||||
include_web_search_request: true,
|
|
||||||
use_streamable_shell_tool: false,
|
|
||||||
include_view_image_tool: true,
|
|
||||||
experimental_unified_exec_tool: true,
|
|
||||||
});
|
});
|
||||||
let (tools, _) = build_specs(
|
let (tools, _) = build_specs(
|
||||||
&config,
|
&config,
|
||||||
@@ -1106,14 +1096,11 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_build_specs_mcp_tools_sorted_by_name() {
|
fn test_build_specs_mcp_tools_sorted_by_name() {
|
||||||
let model_family = find_family_for_model("o3").expect("o3 should be a valid model family");
|
let model_family = find_family_for_model("o3").expect("o3 should be a valid model family");
|
||||||
|
let mut features = Features::with_defaults();
|
||||||
|
features.enable(Feature::UnifiedExec);
|
||||||
let config = ToolsConfig::new(&ToolsConfigParams {
|
let config = ToolsConfig::new(&ToolsConfigParams {
|
||||||
model_family: &model_family,
|
model_family: &model_family,
|
||||||
include_plan_tool: false,
|
features: &features,
|
||||||
include_apply_patch_tool: false,
|
|
||||||
include_web_search_request: false,
|
|
||||||
use_streamable_shell_tool: false,
|
|
||||||
include_view_image_tool: true,
|
|
||||||
experimental_unified_exec_tool: true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Intentionally construct a map with keys that would sort alphabetically.
|
// Intentionally construct a map with keys that would sort alphabetically.
|
||||||
@@ -1183,14 +1170,12 @@ mod tests {
|
|||||||
fn test_mcp_tool_property_missing_type_defaults_to_string() {
|
fn test_mcp_tool_property_missing_type_defaults_to_string() {
|
||||||
let model_family = find_family_for_model("gpt-5-codex")
|
let model_family = find_family_for_model("gpt-5-codex")
|
||||||
.expect("gpt-5-codex should be a valid model family");
|
.expect("gpt-5-codex should be a valid model family");
|
||||||
|
let mut features = Features::with_defaults();
|
||||||
|
features.enable(Feature::UnifiedExec);
|
||||||
|
features.enable(Feature::WebSearchRequest);
|
||||||
let config = ToolsConfig::new(&ToolsConfigParams {
|
let config = ToolsConfig::new(&ToolsConfigParams {
|
||||||
model_family: &model_family,
|
model_family: &model_family,
|
||||||
include_plan_tool: false,
|
features: &features,
|
||||||
include_apply_patch_tool: false,
|
|
||||||
include_web_search_request: true,
|
|
||||||
use_streamable_shell_tool: false,
|
|
||||||
include_view_image_tool: true,
|
|
||||||
experimental_unified_exec_tool: true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let (tools, _) = build_specs(
|
let (tools, _) = build_specs(
|
||||||
@@ -1252,14 +1237,12 @@ mod tests {
|
|||||||
fn test_mcp_tool_integer_normalized_to_number() {
|
fn test_mcp_tool_integer_normalized_to_number() {
|
||||||
let model_family = find_family_for_model("gpt-5-codex")
|
let model_family = find_family_for_model("gpt-5-codex")
|
||||||
.expect("gpt-5-codex should be a valid model family");
|
.expect("gpt-5-codex should be a valid model family");
|
||||||
|
let mut features = Features::with_defaults();
|
||||||
|
features.enable(Feature::UnifiedExec);
|
||||||
|
features.enable(Feature::WebSearchRequest);
|
||||||
let config = ToolsConfig::new(&ToolsConfigParams {
|
let config = ToolsConfig::new(&ToolsConfigParams {
|
||||||
model_family: &model_family,
|
model_family: &model_family,
|
||||||
include_plan_tool: false,
|
features: &features,
|
||||||
include_apply_patch_tool: false,
|
|
||||||
include_web_search_request: true,
|
|
||||||
use_streamable_shell_tool: false,
|
|
||||||
include_view_image_tool: true,
|
|
||||||
experimental_unified_exec_tool: true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let (tools, _) = build_specs(
|
let (tools, _) = build_specs(
|
||||||
@@ -1316,14 +1299,13 @@ mod tests {
|
|||||||
fn test_mcp_tool_array_without_items_gets_default_string_items() {
|
fn test_mcp_tool_array_without_items_gets_default_string_items() {
|
||||||
let model_family = find_family_for_model("gpt-5-codex")
|
let model_family = find_family_for_model("gpt-5-codex")
|
||||||
.expect("gpt-5-codex should be a valid model family");
|
.expect("gpt-5-codex should be a valid model family");
|
||||||
|
let mut features = Features::with_defaults();
|
||||||
|
features.enable(Feature::UnifiedExec);
|
||||||
|
features.enable(Feature::WebSearchRequest);
|
||||||
|
features.enable(Feature::ApplyPatchFreeform);
|
||||||
let config = ToolsConfig::new(&ToolsConfigParams {
|
let config = ToolsConfig::new(&ToolsConfigParams {
|
||||||
model_family: &model_family,
|
model_family: &model_family,
|
||||||
include_plan_tool: false,
|
features: &features,
|
||||||
include_apply_patch_tool: true,
|
|
||||||
include_web_search_request: true,
|
|
||||||
use_streamable_shell_tool: false,
|
|
||||||
include_view_image_tool: true,
|
|
||||||
experimental_unified_exec_tool: true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let (tools, _) = build_specs(
|
let (tools, _) = build_specs(
|
||||||
@@ -1383,14 +1365,12 @@ mod tests {
|
|||||||
fn test_mcp_tool_anyof_defaults_to_string() {
|
fn test_mcp_tool_anyof_defaults_to_string() {
|
||||||
let model_family = find_family_for_model("gpt-5-codex")
|
let model_family = find_family_for_model("gpt-5-codex")
|
||||||
.expect("gpt-5-codex should be a valid model family");
|
.expect("gpt-5-codex should be a valid model family");
|
||||||
|
let mut features = Features::with_defaults();
|
||||||
|
features.enable(Feature::UnifiedExec);
|
||||||
|
features.enable(Feature::WebSearchRequest);
|
||||||
let config = ToolsConfig::new(&ToolsConfigParams {
|
let config = ToolsConfig::new(&ToolsConfigParams {
|
||||||
model_family: &model_family,
|
model_family: &model_family,
|
||||||
include_plan_tool: false,
|
features: &features,
|
||||||
include_apply_patch_tool: false,
|
|
||||||
include_web_search_request: true,
|
|
||||||
use_streamable_shell_tool: false,
|
|
||||||
include_view_image_tool: true,
|
|
||||||
experimental_unified_exec_tool: true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let (tools, _) = build_specs(
|
let (tools, _) = build_specs(
|
||||||
@@ -1462,14 +1442,12 @@ mod tests {
|
|||||||
fn test_get_openai_tools_mcp_tools_with_additional_properties_schema() {
|
fn test_get_openai_tools_mcp_tools_with_additional_properties_schema() {
|
||||||
let model_family = find_family_for_model("gpt-5-codex")
|
let model_family = find_family_for_model("gpt-5-codex")
|
||||||
.expect("gpt-5-codex should be a valid model family");
|
.expect("gpt-5-codex should be a valid model family");
|
||||||
|
let mut features = Features::with_defaults();
|
||||||
|
features.enable(Feature::UnifiedExec);
|
||||||
|
features.enable(Feature::WebSearchRequest);
|
||||||
let config = ToolsConfig::new(&ToolsConfigParams {
|
let config = ToolsConfig::new(&ToolsConfigParams {
|
||||||
model_family: &model_family,
|
model_family: &model_family,
|
||||||
include_plan_tool: false,
|
features: &features,
|
||||||
include_apply_patch_tool: false,
|
|
||||||
include_web_search_request: true,
|
|
||||||
use_streamable_shell_tool: false,
|
|
||||||
include_view_image_tool: true,
|
|
||||||
experimental_unified_exec_tool: true,
|
|
||||||
});
|
});
|
||||||
let (tools, _) = build_specs(
|
let (tools, _) = build_specs(
|
||||||
&config,
|
&config,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use codex_core::CodexAuth;
|
|||||||
use codex_core::ConversationManager;
|
use codex_core::ConversationManager;
|
||||||
use codex_core::ModelProviderInfo;
|
use codex_core::ModelProviderInfo;
|
||||||
use codex_core::built_in_model_providers;
|
use codex_core::built_in_model_providers;
|
||||||
|
use codex_core::features::Feature;
|
||||||
use codex_core::model_family::find_family_for_model;
|
use codex_core::model_family::find_family_for_model;
|
||||||
use codex_core::protocol::EventMsg;
|
use codex_core::protocol::EventMsg;
|
||||||
use codex_core::protocol::InputItem;
|
use codex_core::protocol::InputItem;
|
||||||
@@ -56,12 +57,12 @@ async fn collect_tool_identifiers_for_model(model: &str) -> Vec<String> {
|
|||||||
config.model = model.to_string();
|
config.model = model.to_string();
|
||||||
config.model_family =
|
config.model_family =
|
||||||
find_family_for_model(model).unwrap_or_else(|| panic!("unknown model family for {model}"));
|
find_family_for_model(model).unwrap_or_else(|| panic!("unknown model family for {model}"));
|
||||||
config.include_plan_tool = false;
|
config.features.disable(Feature::PlanTool);
|
||||||
config.include_apply_patch_tool = false;
|
config.features.disable(Feature::ApplyPatchFreeform);
|
||||||
config.include_view_image_tool = false;
|
config.features.disable(Feature::ViewImageTool);
|
||||||
config.tools_web_search_request = false;
|
config.features.disable(Feature::WebSearchRequest);
|
||||||
config.use_experimental_streamable_shell_tool = false;
|
config.features.disable(Feature::StreamableShell);
|
||||||
config.use_experimental_unified_exec_tool = false;
|
config.features.disable(Feature::UnifiedExec);
|
||||||
|
|
||||||
let conversation_manager =
|
let conversation_manager =
|
||||||
ConversationManager::with_auth(CodexAuth::from_api_key("Test API Key"));
|
ConversationManager::with_auth(CodexAuth::from_api_key("Test API Key"));
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use codex_core::ConversationManager;
|
|||||||
use codex_core::ModelProviderInfo;
|
use codex_core::ModelProviderInfo;
|
||||||
use codex_core::built_in_model_providers;
|
use codex_core::built_in_model_providers;
|
||||||
use codex_core::config::OPENAI_DEFAULT_MODEL;
|
use codex_core::config::OPENAI_DEFAULT_MODEL;
|
||||||
|
use codex_core::features::Feature;
|
||||||
use codex_core::model_family::find_family_for_model;
|
use codex_core::model_family::find_family_for_model;
|
||||||
use codex_core::protocol::AskForApproval;
|
use codex_core::protocol::AskForApproval;
|
||||||
use codex_core::protocol::EventMsg;
|
use codex_core::protocol::EventMsg;
|
||||||
@@ -99,10 +100,10 @@ async fn codex_mini_latest_tools() {
|
|||||||
config.cwd = cwd.path().to_path_buf();
|
config.cwd = cwd.path().to_path_buf();
|
||||||
config.model_provider = model_provider;
|
config.model_provider = model_provider;
|
||||||
config.user_instructions = Some("be consistent and helpful".to_string());
|
config.user_instructions = Some("be consistent and helpful".to_string());
|
||||||
|
config.features.disable(Feature::ApplyPatchFreeform);
|
||||||
|
|
||||||
let conversation_manager =
|
let conversation_manager =
|
||||||
ConversationManager::with_auth(CodexAuth::from_api_key("Test API Key"));
|
ConversationManager::with_auth(CodexAuth::from_api_key("Test API Key"));
|
||||||
config.include_apply_patch_tool = false;
|
|
||||||
config.model = "codex-mini-latest".to_string();
|
config.model = "codex-mini-latest".to_string();
|
||||||
config.model_family = find_family_for_model("codex-mini-latest").unwrap();
|
config.model_family = find_family_for_model("codex-mini-latest").unwrap();
|
||||||
|
|
||||||
@@ -185,7 +186,7 @@ async fn prompt_tools_are_consistent_across_requests() {
|
|||||||
config.cwd = cwd.path().to_path_buf();
|
config.cwd = cwd.path().to_path_buf();
|
||||||
config.model_provider = model_provider;
|
config.model_provider = model_provider;
|
||||||
config.user_instructions = Some("be consistent and helpful".to_string());
|
config.user_instructions = Some("be consistent and helpful".to_string());
|
||||||
config.include_plan_tool = true;
|
config.features.enable(Feature::PlanTool);
|
||||||
|
|
||||||
let conversation_manager =
|
let conversation_manager =
|
||||||
ConversationManager::with_auth(CodexAuth::from_api_key("Test API Key"));
|
ConversationManager::with_auth(CodexAuth::from_api_key("Test API Key"));
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use std::time::UNIX_EPOCH;
|
|||||||
|
|
||||||
use codex_core::config_types::McpServerConfig;
|
use codex_core::config_types::McpServerConfig;
|
||||||
use codex_core::config_types::McpServerTransportConfig;
|
use codex_core::config_types::McpServerTransportConfig;
|
||||||
|
use codex_core::features::Feature;
|
||||||
|
|
||||||
use codex_core::protocol::AskForApproval;
|
use codex_core::protocol::AskForApproval;
|
||||||
use codex_core::protocol::EventMsg;
|
use codex_core::protocol::EventMsg;
|
||||||
@@ -74,7 +75,7 @@ async fn stdio_server_round_trip() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
let fixture = test_codex()
|
let fixture = test_codex()
|
||||||
.with_config(move |config| {
|
.with_config(move |config| {
|
||||||
config.use_experimental_use_rmcp_client = true;
|
config.features.enable(Feature::RmcpClient);
|
||||||
config.mcp_servers.insert(
|
config.mcp_servers.insert(
|
||||||
server_name.to_string(),
|
server_name.to_string(),
|
||||||
McpServerConfig {
|
McpServerConfig {
|
||||||
@@ -227,7 +228,7 @@ async fn streamable_http_tool_call_round_trip() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
let fixture = test_codex()
|
let fixture = test_codex()
|
||||||
.with_config(move |config| {
|
.with_config(move |config| {
|
||||||
config.use_experimental_use_rmcp_client = true;
|
config.features.enable(Feature::RmcpClient);
|
||||||
config.mcp_servers.insert(
|
config.mcp_servers.insert(
|
||||||
server_name.to_string(),
|
server_name.to_string(),
|
||||||
McpServerConfig {
|
McpServerConfig {
|
||||||
@@ -408,7 +409,7 @@ async fn streamable_http_with_oauth_round_trip() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
let fixture = test_codex()
|
let fixture = test_codex()
|
||||||
.with_config(move |config| {
|
.with_config(move |config| {
|
||||||
config.use_experimental_use_rmcp_client = true;
|
config.features.enable(Feature::RmcpClient);
|
||||||
config.mcp_servers.insert(
|
config.mcp_servers.insert(
|
||||||
server_name.to_string(),
|
server_name.to_string(),
|
||||||
McpServerConfig {
|
McpServerConfig {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#![cfg(not(target_os = "windows"))]
|
#![cfg(not(target_os = "windows"))]
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use codex_core::features::Feature;
|
||||||
use codex_core::model_family::find_family_for_model;
|
use codex_core::model_family::find_family_for_model;
|
||||||
use codex_core::protocol::AskForApproval;
|
use codex_core::protocol::AskForApproval;
|
||||||
use codex_core::protocol::EventMsg;
|
use codex_core::protocol::EventMsg;
|
||||||
@@ -77,7 +78,7 @@ async fn shell_output_stays_json_without_freeform_apply_patch() -> Result<()> {
|
|||||||
|
|
||||||
let server = start_mock_server().await;
|
let server = start_mock_server().await;
|
||||||
let mut builder = test_codex().with_config(|config| {
|
let mut builder = test_codex().with_config(|config| {
|
||||||
config.include_apply_patch_tool = false;
|
config.features.disable(Feature::ApplyPatchFreeform);
|
||||||
config.model = "gpt-5".to_string();
|
config.model = "gpt-5".to_string();
|
||||||
config.model_family = find_family_for_model("gpt-5").expect("gpt-5 is a model family");
|
config.model_family = find_family_for_model("gpt-5").expect("gpt-5 is a model family");
|
||||||
});
|
});
|
||||||
@@ -143,7 +144,7 @@ async fn shell_output_is_structured_with_freeform_apply_patch() -> Result<()> {
|
|||||||
|
|
||||||
let server = start_mock_server().await;
|
let server = start_mock_server().await;
|
||||||
let mut builder = test_codex().with_config(|config| {
|
let mut builder = test_codex().with_config(|config| {
|
||||||
config.include_apply_patch_tool = true;
|
config.features.enable(Feature::ApplyPatchFreeform);
|
||||||
});
|
});
|
||||||
let test = builder.build(&server).await?;
|
let test = builder.build(&server).await?;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#![cfg(not(target_os = "windows"))]
|
#![cfg(not(target_os = "windows"))]
|
||||||
|
|
||||||
use assert_matches::assert_matches;
|
use assert_matches::assert_matches;
|
||||||
|
use codex_core::features::Feature;
|
||||||
use codex_core::model_family::find_family_for_model;
|
use codex_core::model_family::find_family_for_model;
|
||||||
use codex_core::protocol::AskForApproval;
|
use codex_core::protocol::AskForApproval;
|
||||||
use codex_core::protocol::EventMsg;
|
use codex_core::protocol::EventMsg;
|
||||||
@@ -104,7 +105,7 @@ async fn update_plan_tool_emits_plan_update_event() -> anyhow::Result<()> {
|
|||||||
let server = start_mock_server().await;
|
let server = start_mock_server().await;
|
||||||
|
|
||||||
let mut builder = test_codex().with_config(|config| {
|
let mut builder = test_codex().with_config(|config| {
|
||||||
config.include_plan_tool = true;
|
config.features.enable(Feature::PlanTool);
|
||||||
});
|
});
|
||||||
let TestCodex {
|
let TestCodex {
|
||||||
codex,
|
codex,
|
||||||
@@ -191,7 +192,7 @@ async fn update_plan_tool_rejects_malformed_payload() -> anyhow::Result<()> {
|
|||||||
let server = start_mock_server().await;
|
let server = start_mock_server().await;
|
||||||
|
|
||||||
let mut builder = test_codex().with_config(|config| {
|
let mut builder = test_codex().with_config(|config| {
|
||||||
config.include_plan_tool = true;
|
config.features.enable(Feature::PlanTool);
|
||||||
});
|
});
|
||||||
let TestCodex {
|
let TestCodex {
|
||||||
codex,
|
codex,
|
||||||
@@ -285,7 +286,7 @@ async fn apply_patch_tool_executes_and_emits_patch_events() -> anyhow::Result<()
|
|||||||
let server = start_mock_server().await;
|
let server = start_mock_server().await;
|
||||||
|
|
||||||
let mut builder = test_codex().with_config(|config| {
|
let mut builder = test_codex().with_config(|config| {
|
||||||
config.include_apply_patch_tool = true;
|
config.features.enable(Feature::ApplyPatchFreeform);
|
||||||
});
|
});
|
||||||
let TestCodex {
|
let TestCodex {
|
||||||
codex,
|
codex,
|
||||||
@@ -403,7 +404,7 @@ async fn apply_patch_reports_parse_diagnostics() -> anyhow::Result<()> {
|
|||||||
let server = start_mock_server().await;
|
let server = start_mock_server().await;
|
||||||
|
|
||||||
let mut builder = test_codex().with_config(|config| {
|
let mut builder = test_codex().with_config(|config| {
|
||||||
config.include_apply_patch_tool = true;
|
config.features.enable(Feature::ApplyPatchFreeform);
|
||||||
});
|
});
|
||||||
let TestCodex {
|
let TestCodex {
|
||||||
codex,
|
codex,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
#![allow(clippy::unwrap_used, clippy::expect_used)]
|
#![allow(clippy::unwrap_used, clippy::expect_used)]
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use codex_core::features::Feature;
|
||||||
use codex_core::model_family::find_family_for_model;
|
use codex_core::model_family::find_family_for_model;
|
||||||
use codex_core::protocol::AskForApproval;
|
use codex_core::protocol::AskForApproval;
|
||||||
use codex_core::protocol::EventMsg;
|
use codex_core::protocol::EventMsg;
|
||||||
@@ -293,7 +294,11 @@ async fn collect_tools(use_unified_exec: bool) -> Result<Vec<String>> {
|
|||||||
let mock = mount_sse_sequence(&server, responses).await;
|
let mock = mount_sse_sequence(&server, responses).await;
|
||||||
|
|
||||||
let mut builder = test_codex().with_config(move |config| {
|
let mut builder = test_codex().with_config(move |config| {
|
||||||
config.use_experimental_unified_exec_tool = use_unified_exec;
|
if use_unified_exec {
|
||||||
|
config.features.enable(Feature::UnifiedExec);
|
||||||
|
} else {
|
||||||
|
config.features.disable(Feature::UnifiedExec);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
let test = builder.build(&server).await?;
|
let test = builder.build(&server).await?;
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use codex_core::features::Feature;
|
||||||
use codex_core::protocol::AskForApproval;
|
use codex_core::protocol::AskForApproval;
|
||||||
use codex_core::protocol::EventMsg;
|
use codex_core::protocol::EventMsg;
|
||||||
use codex_core::protocol::InputItem;
|
use codex_core::protocol::InputItem;
|
||||||
@@ -42,7 +43,13 @@ fn collect_tool_outputs(bodies: &[Value]) -> Result<HashMap<String, Value>> {
|
|||||||
if let Some(call_id) = item.get("call_id").and_then(Value::as_str) {
|
if let Some(call_id) = item.get("call_id").and_then(Value::as_str) {
|
||||||
let content = extract_output_text(item)
|
let content = extract_output_text(item)
|
||||||
.ok_or_else(|| anyhow::anyhow!("missing tool output content"))?;
|
.ok_or_else(|| anyhow::anyhow!("missing tool output content"))?;
|
||||||
let parsed: Value = serde_json::from_str(content)?;
|
let trimmed = content.trim();
|
||||||
|
if trimmed.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let parsed: Value = serde_json::from_str(trimmed).map_err(|err| {
|
||||||
|
anyhow::anyhow!("failed to parse tool output content {trimmed:?}: {err}")
|
||||||
|
})?;
|
||||||
outputs.insert(call_id.to_string(), parsed);
|
outputs.insert(call_id.to_string(), parsed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -59,7 +66,7 @@ async fn unified_exec_reuses_session_via_stdin() -> Result<()> {
|
|||||||
let server = start_mock_server().await;
|
let server = start_mock_server().await;
|
||||||
|
|
||||||
let mut builder = test_codex().with_config(|config| {
|
let mut builder = test_codex().with_config(|config| {
|
||||||
config.use_experimental_unified_exec_tool = true;
|
config.features.enable(Feature::UnifiedExec);
|
||||||
});
|
});
|
||||||
let TestCodex {
|
let TestCodex {
|
||||||
codex,
|
codex,
|
||||||
@@ -176,6 +183,7 @@ async fn unified_exec_streams_after_lagged_output() -> Result<()> {
|
|||||||
|
|
||||||
let mut builder = test_codex().with_config(|config| {
|
let mut builder = test_codex().with_config(|config| {
|
||||||
config.use_experimental_unified_exec_tool = true;
|
config.use_experimental_unified_exec_tool = true;
|
||||||
|
config.features.enable(Feature::UnifiedExec);
|
||||||
});
|
});
|
||||||
let TestCodex {
|
let TestCodex {
|
||||||
codex,
|
codex,
|
||||||
@@ -300,7 +308,7 @@ async fn unified_exec_timeout_and_followup_poll() -> Result<()> {
|
|||||||
let server = start_mock_server().await;
|
let server = start_mock_server().await;
|
||||||
|
|
||||||
let mut builder = test_codex().with_config(|config| {
|
let mut builder = test_codex().with_config(|config| {
|
||||||
config.use_experimental_unified_exec_tool = true;
|
config.features.enable(Feature::UnifiedExec);
|
||||||
});
|
});
|
||||||
let TestCodex {
|
let TestCodex {
|
||||||
codex,
|
codex,
|
||||||
|
|||||||
Reference in New Issue
Block a user