Compare commits

...

1 Commits

Author SHA1 Message Date
Ahmed Ibrahim
66647e7eb8 prefix 2026-02-12 11:06:53 -08:00
4 changed files with 197 additions and 93 deletions

View File

@@ -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")?];

View File

@@ -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.

View File

@@ -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(()));

View File

@@ -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,
}
}