mirror of
https://github.com/openai/codex.git
synced 2026-02-12 20:03:52 +00:00
Compare commits
1 Commits
latest-alp
...
remove_pre
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66647e7eb8 |
@@ -9,6 +9,7 @@ use app_test_support::create_mock_responses_server_sequence_unchecked;
|
||||
use app_test_support::create_shell_command_sse_response;
|
||||
use app_test_support::format_with_current_shell_display;
|
||||
use app_test_support::to_response;
|
||||
use app_test_support::write_models_cache_with_slug_for_originator;
|
||||
use codex_app_server_protocol::ByteRange;
|
||||
use codex_app_server_protocol::ClientInfo;
|
||||
use codex_app_server_protocol::CommandExecutionApprovalDecision;
|
||||
@@ -59,6 +60,7 @@ const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs
|
||||
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
|
||||
const TEST_ORIGINATOR: &str = "codex_vscode";
|
||||
const LOCAL_PRAGMATIC_TEMPLATE: &str = "You are a deeply pragmatic, effective software engineer.";
|
||||
const APP_SERVER_CACHE_ORIGINATOR: &str = "codex_app_server_cache_e2e";
|
||||
|
||||
#[tokio::test]
|
||||
async fn turn_start_sends_originator_header() -> Result<()> {
|
||||
@@ -135,6 +137,89 @@ async fn turn_start_sends_originator_header() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn turn_start_uses_originator_scoped_cache_slug() -> Result<()> {
|
||||
let responses = vec![create_final_assistant_message_sse_response("Done")?];
|
||||
let server = create_mock_responses_server_sequence_unchecked(responses).await;
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(
|
||||
codex_home.path(),
|
||||
&server.uri(),
|
||||
"never",
|
||||
&BTreeMap::from([(Feature::Personality, true)]),
|
||||
)?;
|
||||
let cached_slug = "app-server-cache-slug-e2e";
|
||||
write_models_cache_with_slug_for_originator(
|
||||
codex_home.path(),
|
||||
APP_SERVER_CACHE_ORIGINATOR,
|
||||
cached_slug,
|
||||
)?;
|
||||
|
||||
let mut mcp = McpProcess::new_with_env(
|
||||
codex_home.path(),
|
||||
&[(
|
||||
codex_core::default_client::CODEX_INTERNAL_ORIGINATOR_OVERRIDE_ENV_VAR,
|
||||
Some(APP_SERVER_CACHE_ORIGINATOR),
|
||||
)],
|
||||
)
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let thread_req = mcp
|
||||
.send_thread_start_request(ThreadStartParams::default())
|
||||
.await?;
|
||||
let thread_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(thread_req)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(thread_resp)?;
|
||||
|
||||
let turn_req = mcp
|
||||
.send_turn_start_request(TurnStartParams {
|
||||
thread_id: thread.id,
|
||||
input: vec![V2UserInput::Text {
|
||||
text: "Hello".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(turn_req)),
|
||||
)
|
||||
.await??;
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("turn/completed"),
|
||||
)
|
||||
.await??;
|
||||
|
||||
let requests = server
|
||||
.received_requests()
|
||||
.await
|
||||
.expect("failed to fetch received requests");
|
||||
let response_request = requests
|
||||
.into_iter()
|
||||
.find(|request| request.url.path().ends_with("/responses"))
|
||||
.expect("expected /responses request");
|
||||
let body: serde_json::Value = serde_json::from_slice(&response_request.body)
|
||||
.expect("responses request body should be json");
|
||||
assert_eq!(body["model"].as_str(), Some(cached_slug));
|
||||
assert!(
|
||||
codex_home
|
||||
.path()
|
||||
.join("models_cache")
|
||||
.join(APP_SERVER_CACHE_ORIGINATOR)
|
||||
.join("models_cache.json")
|
||||
.exists()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn turn_start_emits_user_message_item_with_text_elements() -> Result<()> {
|
||||
let responses = vec![create_final_assistant_message_sse_response("Done")?];
|
||||
|
||||
@@ -137,39 +137,10 @@ impl ModelsManager {
|
||||
// todo(aibrahim): look if we can tighten it to pub(crate)
|
||||
/// Look up model metadata, applying remote overrides and config adjustments.
|
||||
pub async fn get_model_info(&self, model: &str, config: &Config) -> ModelInfo {
|
||||
let remote = self
|
||||
.find_remote_model_by_longest_prefix(model, config)
|
||||
.await;
|
||||
let model = if let Some(remote) = remote {
|
||||
remote
|
||||
} else {
|
||||
model_info::model_info_from_slug(model)
|
||||
};
|
||||
let model = model_info::model_info_from_slug(model);
|
||||
model_info::with_config_overrides(model, config)
|
||||
}
|
||||
|
||||
async fn find_remote_model_by_longest_prefix(
|
||||
&self,
|
||||
model: &str,
|
||||
config: &Config,
|
||||
) -> Option<ModelInfo> {
|
||||
let mut best: Option<ModelInfo> = None;
|
||||
for candidate in self.get_remote_models(config).await {
|
||||
if !model.starts_with(&candidate.slug) {
|
||||
continue;
|
||||
}
|
||||
let is_better_match = if let Some(current) = best.as_ref() {
|
||||
candidate.slug.len() > current.slug.len()
|
||||
} else {
|
||||
true
|
||||
};
|
||||
if is_better_match {
|
||||
best = Some(candidate);
|
||||
}
|
||||
}
|
||||
best
|
||||
}
|
||||
|
||||
/// Refresh models if the provided ETag differs from the cached ETag.
|
||||
///
|
||||
/// Uses `Online` strategy to fetch latest models when ETags differ.
|
||||
|
||||
@@ -55,69 +55,6 @@ use wiremock::MockServer;
|
||||
|
||||
const REMOTE_MODEL_SLUG: &str = "codex-test";
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn remote_models_get_model_info_uses_longest_matching_prefix() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
skip_if_sandbox!(Ok(()));
|
||||
|
||||
let server = MockServer::start().await;
|
||||
let generic = test_remote_model_with_policy(
|
||||
"gpt-5.3",
|
||||
ModelVisibility::List,
|
||||
1_000,
|
||||
TruncationPolicyConfig::bytes(10_000),
|
||||
);
|
||||
let specific = test_remote_model_with_policy(
|
||||
"gpt-5.3-codex",
|
||||
ModelVisibility::List,
|
||||
1_000,
|
||||
TruncationPolicyConfig::bytes(10_000),
|
||||
);
|
||||
let specific = ModelInfo {
|
||||
display_name: "GPT 5.3 Codex".to_string(),
|
||||
base_instructions: "use specific prefix".to_string(),
|
||||
..specific
|
||||
};
|
||||
let generic = ModelInfo {
|
||||
display_name: "GPT 5.3".to_string(),
|
||||
base_instructions: "use generic prefix".to_string(),
|
||||
..generic
|
||||
};
|
||||
mount_models_once(
|
||||
&server,
|
||||
ModelsResponse {
|
||||
models: vec![generic.clone(), specific.clone()],
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
let mut config = load_default_config_for_test(&codex_home).await;
|
||||
config.features.enable(Feature::RemoteModels);
|
||||
|
||||
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
|
||||
let provider = ModelProviderInfo {
|
||||
base_url: Some(format!("{}/v1", server.uri())),
|
||||
..built_in_model_providers()["openai"].clone()
|
||||
};
|
||||
let manager = codex_core::test_support::models_manager_with_provider(
|
||||
codex_home.path().to_path_buf(),
|
||||
codex_core::test_support::auth_manager_from_auth(auth),
|
||||
provider,
|
||||
);
|
||||
|
||||
manager
|
||||
.list_models(&config, RefreshStrategy::OnlineIfUncached)
|
||||
.await;
|
||||
|
||||
let model_info = manager.get_model_info("gpt-5.3-codex-test", &config).await;
|
||||
|
||||
assert_eq!(model_info.slug, specific.slug);
|
||||
assert_eq!(model_info.base_instructions, specific.base_instructions);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn remote_models_remote_model_uses_unified_exec() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
@@ -2,8 +2,19 @@
|
||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||
|
||||
use codex_core::default_client::CODEX_INTERNAL_ORIGINATOR_OVERRIDE_ENV_VAR;
|
||||
use codex_core::models_manager::client_version_to_whole;
|
||||
use codex_core::test_support::all_model_presets;
|
||||
use codex_protocol::openai_models::ConfigShellToolType;
|
||||
use codex_protocol::openai_models::ModelInfo;
|
||||
use codex_protocol::openai_models::ModelPreset;
|
||||
use codex_protocol::openai_models::ModelVisibility;
|
||||
use codex_protocol::openai_models::TruncationPolicyConfig;
|
||||
use codex_protocol::openai_models::default_input_modalities;
|
||||
use core_test_support::responses;
|
||||
use core_test_support::responses::ResponseMock;
|
||||
use core_test_support::test_codex_exec::test_codex_exec;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::path::Path;
|
||||
use wiremock::matchers::header;
|
||||
|
||||
/// Verify that when the server reports an error, `codex-exec` exits with a
|
||||
@@ -52,3 +63,103 @@ async fn supports_originator_override() -> anyhow::Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn uses_codex_exec_scoped_cache_and_sends_cached_slug() -> anyhow::Result<()> {
|
||||
let test = test_codex_exec();
|
||||
let cached_slug = "exec-cache-slug-e2e";
|
||||
write_models_cache_for_originator(test.home_path(), "codex_exec", cached_slug)?;
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
let body = responses::sse(vec![
|
||||
responses::ev_response_created("response_1"),
|
||||
responses::ev_assistant_message("response_1", "Hello, world!"),
|
||||
responses::ev_completed("response_1"),
|
||||
]);
|
||||
let response_mock = responses::mount_sse_once(&server, body).await;
|
||||
|
||||
test.cmd_with_server(&server)
|
||||
.env_remove(CODEX_INTERNAL_ORIGINATOR_OVERRIDE_ENV_VAR)
|
||||
.arg("--skip-git-repo-check")
|
||||
.arg("tell me something")
|
||||
.assert()
|
||||
.code(0);
|
||||
|
||||
assert_response_model_slug(&response_mock, cached_slug);
|
||||
assert!(
|
||||
test.home_path()
|
||||
.join("models_cache")
|
||||
.join("codex_exec")
|
||||
.join("models_cache.json")
|
||||
.exists()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn assert_response_model_slug(response_mock: &ResponseMock, expected_slug: &str) {
|
||||
let request = response_mock.single_request();
|
||||
let request_body = request.body_json();
|
||||
assert_eq!(request_body["model"].as_str(), Some(expected_slug));
|
||||
}
|
||||
|
||||
fn write_models_cache_for_originator(
|
||||
codex_home: &Path,
|
||||
originator: &str,
|
||||
slug: &str,
|
||||
) -> std::io::Result<()> {
|
||||
let Some(first_preset) = all_model_presets()
|
||||
.into_iter()
|
||||
.find(|preset| preset.show_in_picker)
|
||||
else {
|
||||
return Err(std::io::Error::other("no visible model presets"));
|
||||
};
|
||||
let mut model = preset_to_info(&first_preset, 0);
|
||||
model.slug = slug.to_string();
|
||||
let cache_path = codex_home
|
||||
.join("models_cache")
|
||||
.join(originator)
|
||||
.join("models_cache.json");
|
||||
if let Some(parent) = cache_path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
let cache = serde_json::json!({
|
||||
"fetched_at": chrono::Utc::now(),
|
||||
"etag": null,
|
||||
"client_version": client_version_to_whole(),
|
||||
"models": [model]
|
||||
});
|
||||
std::fs::write(cache_path, serde_json::to_string_pretty(&cache)?)
|
||||
}
|
||||
|
||||
fn preset_to_info(preset: &ModelPreset, priority: i32) -> ModelInfo {
|
||||
ModelInfo {
|
||||
slug: preset.id.clone(),
|
||||
display_name: preset.display_name.clone(),
|
||||
description: Some(preset.description.clone()),
|
||||
default_reasoning_level: Some(preset.default_reasoning_effort),
|
||||
supported_reasoning_levels: preset.supported_reasoning_efforts.clone(),
|
||||
shell_type: ConfigShellToolType::ShellCommand,
|
||||
visibility: if preset.show_in_picker {
|
||||
ModelVisibility::List
|
||||
} else {
|
||||
ModelVisibility::Hide
|
||||
},
|
||||
supported_in_api: true,
|
||||
priority,
|
||||
upgrade: preset.upgrade.as_ref().map(|upgrade| upgrade.into()),
|
||||
base_instructions: "base instructions".to_string(),
|
||||
model_messages: None,
|
||||
supports_reasoning_summaries: false,
|
||||
support_verbosity: false,
|
||||
default_verbosity: None,
|
||||
apply_patch_tool_type: None,
|
||||
truncation_policy: TruncationPolicyConfig::bytes(10_000),
|
||||
supports_parallel_tool_calls: false,
|
||||
context_window: Some(272_000),
|
||||
auto_compact_token_limit: None,
|
||||
effective_context_window_percent: 95,
|
||||
experimental_supported_tools: Vec::new(),
|
||||
input_modalities: default_input_modalities(),
|
||||
prefer_websockets: false,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user