Scope model catalog caches to the current provider and auth identity (#43906)

## Why

Switching providers or authentication could reuse a previous identity's model catalog, including its default service tier. An in-flight refresh could also overwrite a newer account's catalog after cache storage completed.

## What changed

- Require a matching provider and auth identity for disk and in-memory catalog reuse. Treat legacy entries without an identity as cache misses and fall back to bundled metadata when the in-memory identity no longer matches.
- Recheck identity after fetching and storing a catalog before publishing it.
- Renew cache freshness only when the client version, identity, and ETag match the stored entry.
- Update cache fixtures to include the configured provider and auth identity.

## Testing

Add regression coverage for provider and auth switches, credential rotation, legacy and mismatched cache entries, conditional TTL renewal, and an account switch during cache storage. Verify that API-key requests do not inherit a ChatGPT catalog's default `service_tier`, while explicit tier selection still works.

GitOrigin-RevId: e15d7ffff238d951d7a1c6a68381cbb479994146
This commit is contained in:
Ahmed Ibrahim authored and copyberry committed 2026-09-08 20:37:30 +00:00
1 parent 4fd2c460dd
commit f046cf35df
35 files changed
+733 -143

No files matched your search

+1 -1
View File
@@ -97,7 +97,7 @@ supports_websockets = false
"#
))
.write(codex_home.path())?;
write_models_cache(codex_home.path())?;
write_models_cache(codex_home.path()).await?;
let mut app_server = TestAppServer::builder()
.with_codex_home(codex_home.path())
.with_env_overrides(&[("OPENAI_API_KEY", None)])
+10 -10
View File
@@ -416,7 +416,7 @@ async fn set_auth_token_updates_account_and_notifies() -> Result<()> {
..Default::default()
},
)?;
write_models_cache(codex_home.path())?;
write_models_cache(codex_home.path()).await?;
let access_token = encode_id_token(
&ChatGptIdTokenClaims::new()
@@ -499,7 +499,7 @@ async fn account_read_refresh_token_is_noop_in_external_mode() -> Result<()> {
..Default::default()
},
)?;
write_models_cache(codex_home.path())?;
write_models_cache(codex_home.path()).await?;
let access_token = encode_id_token(
&ChatGptIdTokenClaims::new()
@@ -611,7 +611,7 @@ async fn external_auth_refreshes_on_unauthorized() -> Result<()> {
..Default::default()
},
)?;
write_models_cache(codex_home.path())?;
write_models_cache(codex_home.path()).await?;
let success_sse = responses::sse(vec![
responses::ev_response_created("resp-turn"),
@@ -726,7 +726,7 @@ async fn external_auth_refresh_error_fails_turn() -> Result<()> {
..Default::default()
},
)?;
write_models_cache(codex_home.path())?;
write_models_cache(codex_home.path()).await?;
let unauthorized = ResponseTemplate::new(401).set_body_json(json!({
"error": { "message": "unauthorized" }
@@ -837,7 +837,7 @@ async fn external_auth_refresh_mismatched_workspace_fails_turn() -> Result<()> {
..Default::default()
},
)?;
write_models_cache(codex_home.path())?;
write_models_cache(codex_home.path()).await?;
let unauthorized = ResponseTemplate::new(401).set_body_json(json!({
"error": { "message": "unauthorized" }
@@ -953,7 +953,7 @@ async fn external_auth_refresh_invalid_access_token_fails_turn() -> Result<()> {
..Default::default()
},
)?;
write_models_cache(codex_home.path())?;
write_models_cache(codex_home.path()).await?;
let unauthorized = ResponseTemplate::new(401).set_body_json(json!({
"error": { "message": "unauthorized" }
@@ -2003,7 +2003,7 @@ async fn login_account_chatgpt_device_code_returns_error_when_disabled() -> Resu
..Default::default()
},
)?;
write_models_cache(codex_home.path())?;
write_models_cache(codex_home.path()).await?;
mock_device_code_usercode_failure(&mock_server, /*status*/ 404).await;
let issuer = mock_server.uri();
@@ -2059,7 +2059,7 @@ async fn login_account_chatgpt_device_code_succeeds_and_notifies() -> Result<()>
..Default::default()
},
)?;
write_models_cache(codex_home.path())?;
write_models_cache(codex_home.path()).await?;
mock_device_code_usercode(&mock_server, /*interval_seconds*/ 0).await;
mock_device_code_token_success(&mock_server).await;
@@ -2139,7 +2139,7 @@ async fn login_account_chatgpt_device_code_failure_notifies_without_account_upda
..Default::default()
},
)?;
write_models_cache(codex_home.path())?;
write_models_cache(codex_home.path()).await?;
mock_device_code_usercode(&mock_server, /*interval_seconds*/ 0).await;
mock_device_code_token_failure(&mock_server, /*status*/ 500).await;
@@ -2210,7 +2210,7 @@ async fn login_account_chatgpt_device_code_can_be_cancelled() -> Result<()> {
..Default::default()
},
)?;
write_models_cache(codex_home.path())?;
write_models_cache(codex_home.path()).await?;
mock_device_code_usercode(&mock_server, /*interval_seconds*/ 1).await;
mock_device_code_token_failure(&mock_server, /*status*/ 404).await;
@@ -56,7 +56,6 @@ async fn attestation_generate_round_trip_adds_header_to_responses_websocket_hand
.await;
let codex_home = TempDir::new()?;
write_models_cache(codex_home.path())?;
create_chatgpt_websocket_config(
codex_home.path(),
&websocket_server.uri().replacen("ws://", "http://", 1),
@@ -66,6 +65,7 @@ async fn attestation_generate_round_trip_adds_header_to_responses_websocket_hand
ChatGptAuthFixture::new("access-chatgpt").plan_type("pro"),
AuthCredentialsStoreMode::File,
)?;
write_models_cache(codex_home.path()).await?;
let mut mcp = TestAppServer::builder()
.with_codex_home(codex_home.path())
@@ -601,7 +601,7 @@ async fn start_function_dynamic_tool_call(call_id: &str) -> Result<PendingDynami
let mut model_info =
codex_core::test_support::construct_model_info_offline("mock-model", &config);
model_info.input_modalities.push(InputModality::Audio);
write_models_cache_with_models(codex_home.path(), vec![model_info])?;
write_models_cache_with_models(codex_home.path(), vec![model_info]).await?;
let mut mcp = TestAppServer::builder()
.with_codex_home(codex_home.path())
@@ -127,7 +127,7 @@ async fn model_guardian_policy_scores_code_mode_cells(
shell: Some(GuardianReviewMode::Adaptive),
..Default::default()
});
write_models_cache_with_models(codex_home.path(), vec![model])?;
write_models_cache_with_models(codex_home.path(), vec![model]).await?;
let mut app_server = TestAppServer::builder()
.with_codex_home(codex_home.path())
.build_initialized_with_timeout(TIMEOUT)
@@ -76,7 +76,7 @@ async fn model_guardian_policy_controls_cua(
computer_use: Some(computer_use),
..Default::default()
});
write_models_cache_with_models(codex_home.path(), vec![model])?;
write_models_cache_with_models(codex_home.path(), vec![model]).await?;
let mut app_server = TestAppServer::builder()
.with_codex_home(codex_home.path())
.build_initialized_with_timeout(TIMEOUT)
@@ -284,7 +284,7 @@ async fn inline_review_delegate_runs_strict_guardian_assessment() -> Result<()>
let mut model = codex_core::test_support::construct_model_info_offline(MODEL, &config);
// Strict MCP approvals must work under the inline delegate's `never` policy.
model.node_repl_auto_review_required = true;
write_models_cache_with_models(codex_home.path(), vec![model])?;
write_models_cache_with_models(codex_home.path(), vec![model]).await?;
let mut app = TestAppServer::builder()
.with_codex_home(codex_home.path())
.build_initialized_with_timeout(TIMEOUT)
@@ -131,7 +131,7 @@ async fn resumed_thread_does_not_wait_for_guardian_websocket_warmup() -> Result<
let config = load_default_config_for_test(&codex_home).await;
let mut model = codex_core::test_support::construct_model_info_offline(MODEL, &config);
model.node_repl_auto_review_required = true;
write_models_cache_with_models(codex_home.path(), vec![model])?;
write_models_cache_with_models(codex_home.path(), vec![model]).await?;
let thread_id = create_fake_rollout(
codex_home.path(),
"2025-01-05T12-00-00",
@@ -833,7 +833,7 @@ async fn guardian_v2_routes_scoped_tool_approvals(
let config = load_default_config_for_test(&codex_home).await;
let mut model_info = codex_core::test_support::construct_model_info_offline(MODEL, &config);
model_info.node_repl_auto_review_required = true;
write_models_cache_with_models(codex_home.path(), vec![model_info])?;
write_models_cache_with_models(codex_home.path(), vec![model_info]).await?;
}
let original_thread_id = match lifecycle {
ThreadLifecycle::New
@@ -2042,7 +2042,7 @@ async fn first_cua_review_does_not_wait_for_initial_score(
let config = load_default_config_for_test(&codex_home).await;
let mut model_info = codex_core::test_support::construct_model_info_offline(MODEL, &config);
model_info.node_repl_auto_review_required = true;
write_models_cache_with_models(codex_home.path(), vec![model_info])?;
write_models_cache_with_models(codex_home.path(), vec![model_info]).await?;
let mut app_server = TestAppServer::builder()
.with_codex_home(codex_home.path())
.build_initialized_with_timeout(TIMEOUT)
@@ -2128,7 +2128,7 @@ async fn user_approval_skips_async_guardian_without_changing_other_modes() -> Re
let config = load_default_config_for_test(&codex_home).await;
let mut model_info = codex_core::test_support::construct_model_info_offline(MODEL, &config);
model_info.node_repl_auto_review_required = true;
write_models_cache_with_models(codex_home.path(), vec![model_info])?;
write_models_cache_with_models(codex_home.path(), vec![model_info]).await?;
let mut app_server = TestAppServer::builder()
.with_codex_home(codex_home.path())
.build_initialized_with_timeout(TIMEOUT)
@@ -314,12 +314,12 @@ async fn guardians_retain_evidence_after_compaction_and_discard_it_after_rollbac
info
})
.collect();
write_models_cache_with_models(codex_home.path(), models)?;
write_chatgpt_auth(
codex_home.path(),
ChatGptAuthFixture::new("access-chatgpt").plan_type("pro"),
AuthCredentialsStoreMode::File,
)?;
write_models_cache_with_models(codex_home.path(), models).await?;
let mut app_server = TestAppServer::builder()
.with_codex_home(codex_home.path())
.with_env_overrides(&[("OPENAI_API_KEY", None)])
@@ -77,7 +77,7 @@ async fn computer_use_scoring_follows_model_review_requirement(
let mut reviewed_model =
codex_core::test_support::construct_model_info_offline(REVIEWED_MODEL, &config);
reviewed_model.node_repl_auto_review_required = true;
write_models_cache_with_models(codex_home.path(), vec![ordinary_model, reviewed_model])?;
write_models_cache_with_models(codex_home.path(), vec![ordinary_model, reviewed_model]).await?;
let mut app_server = TestAppServer::builder()
.with_codex_home(codex_home.path())
.build_initialized_with_timeout(TIMEOUT)
@@ -60,7 +60,7 @@ fn model_from_preset(preset: &ModelPreset) -> Model {
.collect(),
default_reasoning_effort: preset.default_reasoning_effort.clone(),
input_modalities: preset.input_modalities.clone(),
// `write_models_cache()` round-trips through a simplified ModelInfo fixture that does not
// `write_models_cache().await` round-trips through a simplified ModelInfo fixture that does not
// preserve personality placeholders in base instructions, so app-server list results from
// cache report `supports_personality = false`.
// todo(sayan): fix, maybe make roundtrip use ModelInfo only
@@ -101,7 +101,7 @@ fn expected_visible_models() -> Vec<Model> {
#[tokio::test]
async fn list_models_returns_all_models_with_large_limit() -> Result<()> {
let codex_home = TempDir::new()?;
write_models_cache(codex_home.path())?;
write_models_cache(codex_home.path()).await?;
let mut mcp = TestAppServer::builder()
.with_codex_home(codex_home.path())
.without_auto_env()
@@ -131,7 +131,7 @@ async fn list_models_returns_all_models_with_large_limit() -> Result<()> {
#[tokio::test]
async fn list_models_includes_hidden_models() -> Result<()> {
let codex_home = TempDir::new()?;
write_models_cache(codex_home.path())?;
write_models_cache(codex_home.path()).await?;
let mut mcp = TestAppServer::builder()
.with_codex_home(codex_home.path())
.without_auto_env()
@@ -286,7 +286,7 @@ openai_base_url = "{server_uri}/v1"
#[tokio::test]
async fn list_models_pagination_works() -> Result<()> {
let codex_home = TempDir::new()?;
write_models_cache(codex_home.path())?;
write_models_cache(codex_home.path()).await?;
let mut mcp = TestAppServer::builder()
.with_codex_home(codex_home.path())
.without_auto_env()
@@ -332,7 +332,7 @@ async fn list_models_pagination_works() -> Result<()> {
#[tokio::test]
async fn list_models_rejects_invalid_cursor() -> Result<()> {
let codex_home = TempDir::new()?;
write_models_cache(codex_home.path())?;
write_models_cache(codex_home.path()).await?;
let mut mcp = TestAppServer::builder()
.with_codex_home(codex_home.path())
.without_auto_env()
@@ -198,7 +198,7 @@ async fn spawned_subagents_apply_configured_developer_instruction_precedence(
config
.with_extra_config(&feature_config)
.write(codex_home.path())?;
write_models_cache(codex_home.path())?;
write_models_cache(codex_home.path()).await?;
let mut app_server = TestAppServer::builder()
.with_codex_home(codex_home.path())
.build_initialized()
@@ -379,7 +379,7 @@ async fn compacted_full_history_fork_replaces_parent_developer_instructions() ->
"[features.multi_agent_v2]\nenabled = true\nsubagent_developer_instructions = {CHILD_INSTRUCTIONS:?}"
))
.write(codex_home.path())?;
write_models_cache(codex_home.path())?;
write_models_cache(codex_home.path()).await?;
let mut app_server = TestAppServer::builder()
.with_codex_home(codex_home.path())
@@ -641,7 +641,7 @@ async fn cold_resume_preserves_effective_developer_instructions_for_worker(
))
.with_extra_config(&feature_config)
.write(codex_home.path())?;
write_models_cache(codex_home.path())?;
write_models_cache(codex_home.path()).await?;
let (thread_id, child_resume_params, baseline) = {
let mut app_server = TestAppServer::builder()
+1 -1
View File
@@ -48,7 +48,7 @@ async fn account_switch_reloads_telemetry_collectors_and_preserves_trace_context
.email(INITIAL_EMAIL),
AuthCredentialsStoreMode::File,
)?;
write_models_cache(codex_home.path())?;
write_models_cache(codex_home.path()).await?;
let mut app_server = TestAppServer::builder()
.with_codex_home(codex_home.path())
@@ -27,7 +27,7 @@ enum ModelTransport {
Websocket,
}
fn write_provider_config(
async fn write_provider_config(
codex_home: &Path,
model_base_url: &str,
transport: ModelTransport,
@@ -60,7 +60,7 @@ supports_websockets = {supports_websockets}
"#
);
std::fs::write(codex_home.join("config.toml"), config)?;
write_models_cache(codex_home)?;
write_models_cache(codex_home).await?;
Ok(())
}
@@ -98,7 +98,7 @@ async fn managed_residency_overrides_provider_headers(transport: ModelTransport)
};
let codex_home = TempDir::new()?;
write_provider_config(codex_home.path(), &model_base_url, transport)?;
write_provider_config(codex_home.path(), &model_base_url, transport).await?;
std::fs::write(
codex_home.path().join("requirements.toml"),
"enforce_residency = \"us\"\n",
@@ -47,7 +47,7 @@ async fn fork_before_first_turn_preserves_model_selected_multi_agent_version(
let config = load_default_config_for_test(&codex_home).await;
let mut model = codex_core::test_support::construct_model_info_offline("mock-model", &config);
model.multi_agent_version = Some(MultiAgentVersion::V2);
write_models_cache_with_models(codex_home.path(), vec![model])?;
write_models_cache_with_models(codex_home.path(), vec![model]).await?;
let mut mcp = TestAppServer::builder()
.with_codex_home(codex_home.path())
.build_initialized()
@@ -63,7 +63,7 @@ async fn thread_revert_preserves_model_selected_multi_agent_version(restart: boo
let config = load_default_config_for_test(&codex_home).await;
let mut model = codex_core::test_support::construct_model_info_offline("mock-model", &config);
model.multi_agent_version = Some(MultiAgentVersion::V2);
write_models_cache_with_models(codex_home.path(), vec![model])?;
write_models_cache_with_models(codex_home.path(), vec![model]).await?;
let mut mcp = TestAppServer::builder()
.with_codex_home(codex_home.path())
.build()
@@ -47,7 +47,7 @@ async fn thread_settings_update_emits_notification_and_updates_future_turns() ->
.await;
let codex_home = TempDir::new()?;
create_config_toml(codex_home.path(), &server.uri())?;
write_models_cache(codex_home.path())?;
write_models_cache(codex_home.path()).await?;
let (model_id, service_tier_id) = service_tier_model_and_tier_id()?;
let mut mcp = TestAppServer::builder()
@@ -289,7 +289,7 @@ async fn thread_settings_update_null_service_tier_uses_default() -> Result<()> {
.await;
let codex_home = TempDir::new()?;
create_config_toml(codex_home.path(), &server.uri())?;
write_models_cache(codex_home.path())?;
write_models_cache(codex_home.path()).await?;
let (model_id, service_tier_id) = service_tier_model_and_tier_id()?;
let mut mcp = TestAppServer::builder()
@@ -113,7 +113,7 @@ metrics_exporter = {{ otlp-http = {{ endpoint = "{}/metrics", protocol = "json"
.chatgpt_user_id("user-a"),
AuthCredentialsStoreMode::File,
)?;
write_models_cache(home.path())?;
write_models_cache(home.path()).await?;
let loader_overrides = LoaderOverrides::without_managed_config_for_tests();
let config = Arc::new(
ConfigBuilder::default()
@@ -777,7 +777,7 @@ async fn turn_start_emits_thread_scoped_warning_notification_for_trimmed_skills(
MockResponsesConfig::new(&server.uri())
.enable_feature(Feature::Personality)
.write(codex_home.path())?;
write_models_cache(codex_home.path())?;
write_models_cache(codex_home.path()).await?;
let cache_path = codex_home.path().join("models_cache.json");
let mut cache: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&cache_path)?)?;
@@ -874,7 +874,7 @@ async fn turn_start_sends_service_tier_id_to_model_request() -> Result<()> {
let codex_home = TempDir::new()?;
MockResponsesConfig::new(&server.uri()).write(codex_home.path())?;
write_models_cache(codex_home.path())?;
write_models_cache(codex_home.path()).await?;
let service_tier_model = all_model_presets()
.iter()
.find(|preset| preset.show_in_picker && !preset.service_tiers.is_empty())
@@ -998,7 +998,7 @@ async fn turn_start_emits_raw_response_completed_with_upstream_usage(
let codex_home = TempDir::new()?;
MockResponsesConfig::new(&server.uri()).write(codex_home.path())?;
write_models_cache(codex_home.path())?;
write_models_cache(codex_home.path()).await?;
let mut mcp = TestAppServer::builder()
.with_codex_home(codex_home.path())
@@ -3978,7 +3978,7 @@ async fn turn_start_streams_apply_patch_change_updates_v2() -> Result<()> {
.disable_feature(Feature::RemoteModels)
.disable_feature(Feature::ShellSnapshot)
.write(&codex_home)?;
write_models_cache(&codex_home)?;
write_models_cache(&codex_home).await?;
let cache_path = codex_home.join("models_cache.json");
let mut cache: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&cache_path)?)?;
@@ -4368,7 +4368,7 @@ async fn direct_input_to_multi_agent_v2_subagent_is_rejected(
.enable_feature(Feature::Goals)
.with_root_config(&format!("chatgpt_base_url = \"{}\"", server.uri()))
.write(codex_home.path())?;
write_models_cache(codex_home.path())?;
write_models_cache(codex_home.path()).await?;
mount_analytics_capture(&server, codex_home.path()).await?;
let mut mcp = TestAppServer::builder()
@@ -50,7 +50,7 @@ async fn fresh_context_subagent_inherits_disabled_view_image_and_mcp_tools() ->
"[mcp_servers.{TEST_SERVER_NAME}]\nurl = \"{mcp_server_url}/mcp\"\n\n[features.multi_agent_v2]\nenabled = true"
))
.write(codex_home.path())?;
write_models_cache(codex_home.path())?;
write_models_cache(codex_home.path()).await?;
let mut mcp = TestAppServer::builder()
.with_codex_home(codex_home.path())