Files
codex/codex-rs/tools/src/tool_config_tests.rs
T
Ahmed Ibrahim 5577a9e148 [codex] Add model tool mode selector (#25031)
## Why
Some models need to select their code-execution behavior through model
catalog metadata. Models without that metadata must continue to follow
the existing `CodeMode` and `CodeModeOnly` feature flags, including when
a newer server sends an enum value this client does not recognize.

## What changed
- add optional `ModelInfo.tool_mode` metadata with `direct`,
`code_mode`, and `code_mode_only`
- treat omitted and unknown wire values as `None`
- resolve `None` from the existing feature flags
- carry the resolved `ToolMode` directly on `TurnContext`, outside
`Config`
- use the resolved value for turn creation, model switches, review
turns, tool planning, and code execution

## Coverage
- add protocol coverage for omitted, known, and unknown enum values
- add focused coverage for flag fallback and explicit metadata
overriding feature flags
- add core integration coverage that fetches remote model metadata
through `/v1/models` and verifies the outbound `/responses` tools for
explicit `direct` and `code_mode_only` selectors

## Stack
- followed by #25032
2026-05-29 09:05:05 -07:00

156 lines
4.7 KiB
Rust

use codex_features::Feature;
use codex_features::Features;
use codex_protocol::config_types::ModeKind;
use codex_protocol::openai_models::ConfigShellToolType;
use codex_protocol::openai_models::ModelInfo;
use codex_protocol::openai_models::ModelVisibility;
use codex_protocol::openai_models::TruncationPolicyConfig;
use pretty_assertions::assert_eq;
use super::*;
fn model_with_shell_type(shell_type: ConfigShellToolType) -> ModelInfo {
ModelInfo {
slug: "test-model".to_string(),
display_name: "Test Model".to_string(),
description: None,
default_reasoning_level: None,
supported_reasoning_levels: Vec::new(),
shell_type,
visibility: ModelVisibility::List,
supported_in_api: true,
priority: 0,
additional_speed_tiers: Vec::new(),
service_tiers: Vec::new(),
default_service_tier: None,
availability_nux: None,
upgrade: None,
base_instructions: String::new(),
model_messages: None,
supports_reasoning_summaries: false,
default_reasoning_summary: Default::default(),
support_verbosity: false,
default_verbosity: None,
apply_patch_tool_type: None,
web_search_tool_type: Default::default(),
truncation_policy: TruncationPolicyConfig::tokens(/*limit*/ 1024),
supports_parallel_tool_calls: true,
supports_image_detail_original: false,
context_window: None,
max_context_window: None,
auto_compact_token_limit: None,
effective_context_window_percent: 95,
experimental_supported_tools: Vec::new(),
input_modalities: codex_protocol::openai_models::default_input_modalities(),
used_fallback_model_metadata: false,
supports_search_tool: false,
tool_mode: None,
}
}
fn shell_features() -> Features {
let mut features = Features::with_defaults();
features.enable(Feature::ShellTool);
features.disable(Feature::ShellZshFork);
features.disable(Feature::UnifiedExec);
features
}
#[test]
fn shell_type_is_derived_from_model_and_feature_gates() {
let model = model_with_shell_type(ConfigShellToolType::UnifiedExec);
let mut features = shell_features();
assert_eq!(
shell_type_for_model_and_features(&model, &features),
ConfigShellToolType::ShellCommand
);
features.enable(Feature::UnifiedExec);
let expected_unified_exec = if codex_utils_pty::conpty_supported() {
ConfigShellToolType::UnifiedExec
} else {
ConfigShellToolType::ShellCommand
};
assert_eq!(
shell_type_for_model_and_features(&model, &features),
expected_unified_exec
);
features.enable(Feature::ShellZshFork);
assert_eq!(
shell_type_for_model_and_features(&model, &features),
ConfigShellToolType::ShellCommand
);
features.disable(Feature::ShellTool);
assert_eq!(
shell_type_for_model_and_features(&model, &features),
ConfigShellToolType::Disabled
);
}
#[test]
fn shell_command_backend_requires_both_shell_tool_and_zsh_fork() {
let mut features = shell_features();
assert_eq!(
shell_command_backend_for_features(&features),
ShellCommandBackendConfig::Classic
);
features.enable(Feature::ShellZshFork);
assert_eq!(
shell_command_backend_for_features(&features),
ShellCommandBackendConfig::ZshFork
);
features.disable(Feature::ShellTool);
assert_eq!(
shell_command_backend_for_features(&features),
ShellCommandBackendConfig::Classic
);
}
#[test]
fn request_user_input_modes_follow_default_mode_feature() {
let mut features = Features::with_defaults();
features.disable(Feature::DefaultModeRequestUserInput);
assert_eq!(
request_user_input_available_modes(&features),
vec![ModeKind::Plan]
);
features.enable(Feature::DefaultModeRequestUserInput);
assert_eq!(
request_user_input_available_modes(&features),
vec![ModeKind::Default, ModeKind::Plan]
);
}
#[test]
fn unified_exec_shell_mode_uses_zsh_fork_only_when_all_inputs_match() {
let exe = std::env::current_exe().expect("current exe path");
let shell = exe.clone();
let mode = UnifiedExecShellMode::for_session(
ShellCommandBackendConfig::ZshFork,
ToolUserShellType::Zsh,
Some(&shell),
Some(&exe),
);
if cfg!(unix) {
assert!(matches!(mode, UnifiedExecShellMode::ZshFork(_)));
} else {
assert_eq!(mode, UnifiedExecShellMode::Direct);
}
assert_eq!(
UnifiedExecShellMode::for_session(
ShellCommandBackendConfig::Classic,
ToolUserShellType::Zsh,
Some(&shell),
Some(&exe),
),
UnifiedExecShellMode::Direct
);
}