From 0455a92c1f8fc7739a32e92ec38adfb09c4b3d89 Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Mon, 17 Aug 2026 22:51:38 -0400 Subject: [PATCH] fix(codex): allow multiple follow-login providers (#6535) Co-authored-by: Jason --- src-tauri/src/lib.rs | 10 - src-tauri/src/proxy/providers/codex.rs | 171 +++- src-tauri/src/services/provider/live.rs | 95 +- src-tauri/src/services/provider/mod.rs | 818 ++---------------- src-tauri/src/services/proxy.rs | 27 +- src-tauri/tests/provider_service.rs | 18 +- .../providers/AddProviderDialog.tsx | 10 - .../providers/EditProviderDialog.tsx | 21 +- src/components/providers/ProviderCard.tsx | 38 +- .../providers/forms/ProviderForm.tsx | 91 +- src/hooks/useProviderActions.ts | 1 - src/lib/query/mutations.ts | 36 +- src/utils/providerCapabilities.test.ts | 152 +++- src/utils/providerCapabilities.ts | 85 +- src/utils/providerConfigUtils.ts | 32 + tests/components/AddProviderDialog.test.tsx | 6 +- tests/components/EditProviderDialog.test.tsx | 16 +- .../ProviderCard.codexAccount.test.tsx | 100 ++- .../ProviderForm.codexManagedAccount.test.tsx | 275 ++---- tests/hooks/useAddProviderMutation.test.tsx | 67 +- tests/hooks/useProviderActions.test.tsx | 1 + 21 files changed, 852 insertions(+), 1218 deletions(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 6a3de2116..d25b89c02 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -764,16 +764,6 @@ pub fn run() { Err(e) => log::warn!("✗ Failed to seed official providers: {e}"), } - match crate::services::provider::ProviderService::migrate_legacy_codex_official_managed_binding( - &app_state, - ) { - Ok(Some(provider_id)) => log::info!( - "✓ Migrated legacy Codex Official account binding to {provider_id}" - ), - Ok(None) => {} - Err(e) => log::warn!("✗ Failed to migrate legacy Codex Official binding: {e}"), - } - { let db_for_codex_history_migration = app_state.db.clone(); tauri::async_runtime::spawn_blocking(move || { diff --git a/src-tauri/src/proxy/providers/codex.rs b/src-tauri/src/proxy/providers/codex.rs index 2288bb1ba..f335b85d5 100644 --- a/src-tauri/src/proxy/providers/codex.rs +++ b/src-tauri/src/proxy/providers/codex.rs @@ -219,20 +219,91 @@ pub fn provider_needs_responses_namespace_flatten(provider: &Provider) -> bool { provider.is_xai_oauth() } -/// A native-login or managed-account Codex Official card receives -/// authentication from the calling Codex client (`requires_openai_auth = -/// true`). Legacy unbound Official rows keep their previous stored-key behavior. +fn has_explicit_codex_third_party_upstream(provider: &Provider) -> bool { + let non_empty_setting = |key: &str| { + provider + .settings_config + .get(key) + .and_then(JsonValue::as_str) + .is_some_and(|value| !value.trim().is_empty()) + }; + let config = provider + .settings_config + .get("config") + .and_then(JsonValue::as_str) + .map(|text| { + crate::codex_config::strip_codex_unified_session_bucket(text) + .unwrap_or_else(|_| text.to_string()) + }); + let config = config.as_deref(); + + ["baseUrl", "baseURL", "base_url"] + .into_iter() + .any(non_empty_setting) + || config + .and_then(crate::codex_config::extract_codex_experimental_bearer_token) + .is_some() + || config + .and_then(crate::codex_config::extract_codex_base_url) + .is_some() + || config + .and_then(|text| text.parse::().ok()) + .and_then(|doc| { + doc.get("model_provider") + .and_then(TomlValue::as_str) + .map(str::trim) + .filter(|provider_id| !provider_id.is_empty()) + .map(str::to_string) + }) + .is_some_and(|provider_id| !provider_id.eq_ignore_ascii_case("openai")) +} + +/// Codex Official ChatGPT cards receive authentication from the calling Codex +/// client (`requires_openai_auth = true`). Unbound cards with a stored API key +/// stay on the direct OpenAI API path instead of being sent to the ChatGPT +/// backend. The fixed legacy card keeps its existing behavior. pub fn is_codex_official_provider(provider: &Provider) -> bool { - if provider.category.as_deref() != Some("official") { + let is_fixed_official_id = provider.id == crate::database::CODEX_OFFICIAL_PROVIDER_ID; + if is_fixed_official_id && provider.category.as_deref() == Some("official") { + return true; + } + + let has_auth_object = provider + .settings_config + .get("auth") + .is_some_and(JsonValue::is_object); + let has_valid_config_shape = provider + .settings_config + .get("config") + .is_none_or(|config| config.is_null() || config.is_string()); + if !has_auth_object || !has_valid_config_shape { return false; } - provider.id == crate::database::CODEX_OFFICIAL_PROVIDER_ID - || provider - .meta - .as_ref() - .and_then(|meta| meta.managed_account_id_for("codex_oauth")) - .is_some_and(|account_id| !account_id.trim().is_empty()) + if has_explicit_codex_third_party_upstream(provider) { + return false; + } + + let has_managed_account = provider + .meta + .as_ref() + .and_then(|meta| meta.managed_account_id_for("codex_oauth")) + .is_some_and(|account_id| !account_id.trim().is_empty()); + if has_managed_account { + return true; + } + + let has_stored_api_key = provider + .settings_config + .get("auth") + .and_then(|auth| auth.get("OPENAI_API_KEY")) + .and_then(JsonValue::as_str) + .is_some_and(|key| !key.trim().is_empty()); + if has_stored_api_key { + return false; + } + + is_fixed_official_id || provider.category.as_deref() == Some("official") } /// Resolve the model-catalog tool profile for a Codex provider using the SAME @@ -977,17 +1048,26 @@ context_window = 500000 } #[test] - fn official_account_card_uses_fixed_chatgpt_backend_without_stored_key() { - let mut provider = create_provider(json!({ "auth": {}, "config": "" })); - provider.id = "managed-official-account".to_string(); + fn explicit_codex_official_cards_use_chatgpt_backend() { + let mut provider = create_provider(json!({ + "auth": { + "auth_mode": "chatgpt", + "OPENAI_API_KEY": null, + "tokens": { "refresh_token": "legacy-live-only-token" } + }, + "config": "" + })); + provider.id = "unbound-official-account".to_string(); provider.category = Some("official".to_string()); - assert!(!is_codex_official_provider(&provider)); + assert!(is_codex_official_provider(&provider)); let mut native = provider.clone(); native.id = crate::database::CODEX_OFFICIAL_PROVIDER_ID.to_string(); assert!(is_codex_official_provider(&native)); + provider.id = "managed-official-account".to_string(); provider.meta = Some(crate::provider::ProviderMeta { + provider_type: Some("codex_oauth".to_string()), auth_binding: Some(crate::provider::AuthBinding { source: crate::provider::AuthBindingSource::ManagedAccount, auth_provider: Some("codex_oauth".to_string()), @@ -1012,6 +1092,69 @@ context_window = 500000 ), "https://chatgpt.com/backend-api/codex/responses/compact" ); + + let mut official_api_key = create_provider(json!({ + "auth": { "OPENAI_API_KEY": "sk-official" }, + "config": "" + })); + official_api_key.category = Some("official".to_string()); + assert!(!is_codex_official_provider(&official_api_key)); + + let mut stored_bearer = create_provider(json!({ + "auth": {}, + "config": "experimental_bearer_token = \"sk-legacy\"" + })); + stored_bearer.category = Some("official".to_string()); + assert!(!is_codex_official_provider(&stored_bearer)); + + let mut unmarked_custom = create_provider(json!({ + "auth": {}, + "config": "model_provider = \"custom\"\n[model_providers.custom]\nbase_url = \"https://example.com/v1\"" + })); + unmarked_custom.category = Some("official".to_string()); + assert!(!is_codex_official_provider(&unmarked_custom)); + + let mut category_less_fixed_custom = unmarked_custom.clone(); + category_less_fixed_custom.id = crate::database::CODEX_OFFICIAL_PROVIDER_ID.to_string(); + category_less_fixed_custom.category = None; + assert!(!is_codex_official_provider(&category_less_fixed_custom)); + + let mut category_less_fixed_api_key = official_api_key.clone(); + category_less_fixed_api_key.id = crate::database::CODEX_OFFICIAL_PROVIDER_ID.to_string(); + category_less_fixed_api_key.category = None; + assert!(!is_codex_official_provider(&category_less_fixed_api_key)); + + let mut explicit_openai = create_provider(json!({ + "auth": { "OPENAI_API_KEY": "sk-official" }, + "config": "model_provider = \"openai\"" + })); + explicit_openai.category = Some("official".to_string()); + assert!(!is_codex_official_provider(&explicit_openai)); + + let mut managed_with_null_config = provider.clone(); + managed_with_null_config.category = None; + managed_with_null_config.settings_config["config"] = JsonValue::Null; + assert!(is_codex_official_provider(&managed_with_null_config)); + + let mut unified_session = create_provider(json!({ + "auth": {}, + "config": crate::codex_config::inject_codex_unified_session_bucket("") + .expect("inject unified session route") + })); + unified_session.category = Some("official".to_string()); + assert!(is_codex_official_provider(&unified_session)); + + let mut implicit_custom = create_provider(json!({ + "auth": {}, + "config": "model_provider = \"ollama\"" + })); + implicit_custom.category = Some("official".to_string()); + assert!(!is_codex_official_provider(&implicit_custom)); + + let mut grok_official = create_provider(json!({ "config": "" })); + grok_official.id = crate::database::GROKBUILD_OFFICIAL_PROVIDER_ID.to_string(); + grok_official.category = Some("official".to_string()); + assert!(!is_codex_official_provider(&grok_official)); } #[test] diff --git a/src-tauri/src/services/provider/live.rs b/src-tauri/src/services/provider/live.rs index d60cd5dda..81cf5e23e 100644 --- a/src-tauri/src/services/provider/live.rs +++ b/src-tauri/src/services/provider/live.rs @@ -749,19 +749,26 @@ pub(crate) fn build_effective_provider_for_live_with_codex_oauth_manager( let mut effective_provider = provider.clone(); effective_provider.settings_config = build_effective_settings_with_common_config(db, app_type, provider)?; - apply_codex_managed_oauth_auth(app_type, &mut effective_provider, Some(codex_oauth_manager))?; + apply_codex_official_auth(app_type, &mut effective_provider, Some(codex_oauth_manager))?; Ok(effective_provider) } -fn apply_codex_managed_oauth_auth( +fn apply_codex_official_auth( app_type: &AppType, provider: &mut Provider, codex_oauth_manager: Option<&Arc>, ) -> Result<(), AppError> { - if !matches!(app_type, AppType::Codex) || provider.category.as_deref() != Some("official") { + if !matches!(app_type, AppType::Codex) + || !crate::proxy::providers::is_codex_official_provider(provider) + { return Ok(()); } + // Early OAuth builds could bind the fixed card before its category was + // persisted. Normalize only the in-memory live snapshot; the DB row and ID + // remain untouched. + provider.category = Some("official".to_string()); + let Some(account_id) = provider .meta .as_ref() @@ -769,6 +776,9 @@ fn apply_codex_managed_oauth_auth( .map(|id| id.trim().to_string()) .filter(|id| !id.is_empty()) else { + // Preserve the historical unbound Official behavior: an empty stored + // auth follows Codex's current login, while a backfilled login snapshot + // is restored when switching back to this card. return Ok(()); }; @@ -1055,7 +1065,9 @@ fn restore_live_settings_for_provider_backfill( // 统一会话开关注入的共享 `custom` 路由只属于 live 配置;切换回填时 // 必须剥掉,否则官方供应商的存储配置被污染,关闭开关后无法还原。 - if provider.category.as_deref() == Some("official") { + if provider.category.as_deref() == Some("official") + || crate::proxy::providers::is_codex_official_provider(provider) + { if let Err(err) = crate::codex_config::strip_codex_unified_session_bucket_from_settings(&mut settings) { @@ -2698,7 +2710,7 @@ base_url = "https://a.example/v1" } #[test] - fn codex_official_managed_oauth_binding_replaces_auth_with_selected_account_token() { + fn category_less_managed_codex_binding_with_null_config_uses_selected_account_token() { let temp = tempfile::tempdir().expect("tempdir"); let manager = Arc::new(CodexOAuthManager::new(temp.path().to_path_buf())); tauri::async_runtime::block_on(async { @@ -2713,17 +2725,16 @@ base_url = "https://a.example/v1" }); let mut provider = Provider::with_id( - "openai-official".to_string(), + "managed-official".to_string(), "OpenAI Official".to_string(), json!({ "auth": { "OPENAI_API_KEY": "stale-key" }, - "config": "" + "config": null }), None, ); - provider.category = Some("official".to_string()); provider.meta = Some(ProviderMeta { auth_binding: Some(AuthBinding { source: AuthBindingSource::ManagedAccount, @@ -2733,9 +2744,11 @@ base_url = "https://a.example/v1" ..Default::default() }); - apply_codex_managed_oauth_auth(&AppType::Codex, &mut provider, Some(&manager)) + apply_codex_official_auth(&AppType::Codex, &mut provider, Some(&manager)) .expect("apply managed OAuth auth"); + assert_eq!(provider.category.as_deref(), Some("official")); + // last_refresh 是写入时刻的时间戳(非确定),因此逐字段断言而非整体等值。 let auth = provider.settings_config.get("auth").expect("auth written"); assert_eq!( @@ -2773,7 +2786,7 @@ base_url = "https://a.example/v1" } #[test] - fn codex_official_without_binding_does_not_fall_back_to_managed_default_account() { + fn codex_follow_login_without_binding_keeps_stored_auth() { let temp = tempfile::tempdir().expect("tempdir"); let manager = Arc::new(CodexOAuthManager::new(temp.path().to_path_buf())); tauri::async_runtime::block_on(async { @@ -2802,16 +2815,72 @@ base_url = "https://a.example/v1" ); provider.category = Some("official".to_string()); - apply_codex_managed_oauth_auth(&AppType::Codex, &mut provider, Some(&manager)) - .expect("no binding should be a no-op"); + apply_codex_official_auth(&AppType::Codex, &mut provider, Some(&manager)) + .expect("apply follow-login auth policy"); assert_eq!( provider.settings_config.get("auth"), Some(&original_auth), - "unbound official providers must keep native Codex auth instead of using the managed default account" + "follow-login providers must preserve their historical auth snapshot instead of using the managed default" ); } + #[test] + fn follow_login_backfill_preserves_latest_live_tokens() { + let mut provider = Provider::with_id( + "follow-login".to_string(), + "OpenAI Official".to_string(), + json!({ "auth": {}, "config": "" }), + None, + ); + provider.category = Some("official".to_string()); + let mut live_settings = json!({ + "auth": { + "auth_mode": "chatgpt", + "tokens": { + "access_token": "live-access-secret", + "refresh_token": "live-refresh-secret" + } + }, + "config": "" + }); + + strip_codex_managed_oauth_auth_for_backfill(&provider, &mut live_settings); + + assert_eq!( + live_settings["auth"]["tokens"]["refresh_token"], + json!("live-refresh-secret") + ); + } + + #[test] + fn category_less_fixed_follow_login_backfill_preserves_auth_and_strips_live_only_config() { + let provider = Provider::with_id( + crate::database::CODEX_OFFICIAL_PROVIDER_ID.to_string(), + "OpenAI Official".to_string(), + json!({ "auth": {}, "config": "" }), + None, + ); + let injected_config = crate::codex_config::inject_codex_unified_session_bucket("") + .expect("inject unified session bucket"); + let live_settings = json!({ + "auth": { + "auth_mode": "chatgpt", + "tokens": { "refresh_token": "live-refresh-secret" } + }, + "config": injected_config + }); + + let backfilled = + restore_live_settings_for_provider_backfill(&AppType::Codex, &provider, live_settings); + + assert_eq!( + backfilled["auth"]["tokens"]["refresh_token"], + json!("live-refresh-secret") + ); + assert_eq!(backfilled["config"], json!("")); + } + #[test] fn backfill_never_persists_native_tokens_into_managed_provider_config() { // 托管 provider 的存储配置以占位 auth 表示;但 live 里此刻是用户自己 diff --git a/src-tauri/src/services/provider/mod.rs b/src-tauri/src/services/provider/mod.rs index 0865b4754..1695357e9 100644 --- a/src-tauri/src/services/provider/mod.rs +++ b/src-tauri/src/services/provider/mod.rs @@ -71,7 +71,9 @@ pub fn reapply_current_codex_official_live(state: &AppState) -> Result, - ) -> Result<(), AppError> { - if !matches!(app_type, AppType::Codex) { - return Ok(()); - } - - let is_native_login_card = provider.id == crate::database::CODEX_OFFICIAL_PROVIDER_ID; - let has_managed_account = Self::managed_codex_oauth_account_id(provider).is_some(); - - if is_native_login_card && has_managed_account { - return Err(AppError::localized( - "provider.codex.nativeLoginCard.cannotBind", - "Codex 当前登录卡片不能绑定托管账号,请新增一张 OpenAI Official 卡片", - "The Codex current-login card cannot bind a managed account; add another OpenAI Official card instead", - )); - } - if provider.category.as_deref() != Some("official") { - return Ok(()); - } - let preserves_legacy_unbound_card = existing_provider.is_some_and(|existing| { - existing.id != crate::database::CODEX_OFFICIAL_PROVIDER_ID - && existing.category.as_deref() == Some("official") - && Self::managed_codex_oauth_account_id(existing).is_none() - }); - if !is_native_login_card && !has_managed_account && !preserves_legacy_unbound_card { - return Err(AppError::localized( - "provider.codex.managedOfficial.accountRequired", - "新增的 OpenAI Official 卡片必须绑定托管账号", - "A new OpenAI Official card must bind a managed account", - )); - } - - Ok(()) - } - - fn matches_interrupted_codex_official_migration( - original: &Provider, - candidate: &Provider, - ) -> bool { - let mut expected = original.clone(); - expected.id = candidate.id.clone(); - expected.category = Some("official".to_string()); - expected.created_at = candidate.created_at; - expected.sort_index = candidate.sort_index; - expected.in_failover_queue = candidate.in_failover_queue; - serde_json::to_value(expected).ok() == serde_json::to_value(candidate).ok() - } - - /// Upgrade the early PR shape where a managed account was bound directly - /// to the fixed `codex-official` row. The account keeps an independent row - /// and remains selected, while the fixed ID returns to native-login use. - pub(crate) fn migrate_legacy_codex_official_managed_binding( - state: &AppState, - ) -> Result, AppError> { - let app_type = AppType::Codex; - let app_type_str = app_type.as_str(); - let fixed_id = crate::database::CODEX_OFFICIAL_PROVIDER_ID; - let Some(original) = state.db.get_provider_by_id(fixed_id, app_type_str)? else { - // Deleting the native-login card is supported. It is recreated only - // when the user explicitly adds "Use Codex current login" again. - return Ok(None); - }; - let Some(account_id) = Self::managed_codex_oauth_account_id(&original) else { - return Ok(None); - }; - - let db_current = state.db.get_current_provider(app_type_str)?; - let local_current = crate::settings::get_current_provider(&app_type); - let providers = state.db.get_all_providers(app_type_str)?; - let existing_managed = providers.values().find(|provider| { - provider.id != fixed_id - && provider.category.as_deref() == Some("official") - && Self::managed_codex_oauth_account_id(provider).as_deref() - == Some(account_id.as_str()) - && Self::matches_interrupted_codex_official_migration(&original, provider) - }); - let new_id = existing_managed - .map(|provider| provider.id.clone()) - .unwrap_or_else(|| uuid::Uuid::new_v4().to_string()); - let managed_to_create = existing_managed.is_none().then(|| { - let mut managed = original.clone(); - managed.id = new_id.clone(); - managed.created_at = Some(chrono::Utc::now().timestamp_millis()); - managed.sort_index = Some( - providers - .values() - .filter_map(|provider| provider.sort_index) - .max() - .map_or(0, |index| index + 1), - ); - managed.category = Some("official".to_string()); - managed.in_failover_queue = false; - managed - }); - - let mut native = original.clone(); - native.category = Some("official".to_string()); - native.in_failover_queue = false; - if let Some(root) = native.settings_config.as_object_mut() { - root.insert("auth".to_string(), serde_json::json!({})); - } - if let Some(meta) = native.meta.as_mut() { - meta.auth_binding = None; - if meta.provider_type.as_deref() == Some("codex_oauth") { - meta.provider_type = None; - } - } - - let migrate_result = (|| -> Result<(), AppError> { - if let Some(managed) = managed_to_create.as_ref() { - state.db.save_provider(app_type_str, managed)?; - } - if db_current.as_deref() == Some(fixed_id) { - state.db.set_current_provider(app_type_str, &new_id)?; - } - if local_current.as_deref() == Some(fixed_id) { - crate::settings::set_current_provider(&app_type, Some(&new_id))?; - } - state - .db - .remove_from_failover_queue(app_type_str, fixed_id)?; - // Clearing the fixed binding is the completion marker. If the - // process exits before this final write, the next startup can find - // the matching managed clone and resume the remaining steps. - state.db.save_provider(app_type_str, &native)?; - Ok(()) - })(); - - if let Err(error) = migrate_result { - let mut rollback_failures = Vec::new(); - if let Err(rollback_error) = state.db.save_provider(app_type_str, &original) { - rollback_failures.push(format!("restore fixed row: {rollback_error}")); - } - if original.in_failover_queue { - if let Err(rollback_error) = state.db.add_to_failover_queue(app_type_str, fixed_id) - { - rollback_failures - .push(format!("restore failover membership: {rollback_error}")); - } - } - if let Some(current_id) = db_current.as_deref() { - if let Err(rollback_error) = state.db.set_current_provider(app_type_str, current_id) - { - rollback_failures.push(format!("restore database current: {rollback_error}")); - } - } - if managed_to_create.is_some() { - if let Err(rollback_error) = state.db.delete_provider(app_type_str, &new_id) { - rollback_failures.push(format!("remove migrated row: {rollback_error}")); - } - } - if local_current.as_deref() == Some(fixed_id) { - if let Err(rollback_error) = - crate::settings::set_current_provider(&app_type, Some(fixed_id)) - { - rollback_failures.push(format!("restore local current: {rollback_error}")); - } - } - - return if rollback_failures.is_empty() { - Err(error) - } else { - Err(AppError::Message(format!( - "迁移旧 Codex Official 账号卡失败: {error}; 回滚失败: {}", - rollback_failures.join("; ") - ))) - }; - } - - Ok(Some(new_id)) - } - /// 提交 current(settings/DB)前的预检:若目标是托管 Codex official provider, /// 先解析一次有效 live 配置(会联网换取并缓存 token)。同时返回这份已解析配置, /// 让后续落盘直接复用同一 token bundle,避免一次操作重复解析/刷新。 @@ -4833,7 +4284,6 @@ impl ProviderService { // Normalize Claude model keys Self::normalize_provider_if_claude(&app_type, &mut provider); Self::validate_provider_settings(&app_type, &provider)?; - Self::validate_codex_official_card_identity(&app_type, &provider, None)?; normalize_provider_common_config_for_storage(state.db.as_ref(), &app_type, &mut provider)?; Self::normalize_usage_script_credential_overrides(&app_type, &mut provider); if app_type.is_additive_mode() { @@ -4970,85 +4420,15 @@ impl ProviderService { // Normalize Claude model keys Self::normalize_provider_if_claude(&app_type, &mut provider); Self::validate_provider_settings(&app_type, &provider)?; - Self::validate_codex_official_card_identity( - &app_type, - &provider, - existing_provider.as_ref(), - )?; normalize_provider_common_config_for_storage(state.db.as_ref(), &app_type, &mut provider)?; + if matches!(app_type, AppType::Codex) && provider.category.as_deref() == Some("official") { + crate::codex_config::strip_codex_unified_session_bucket_from_settings( + &mut provider.settings_config, + )?; + } Self::normalize_usage_script_credential_overrides(&app_type, &mut provider); - // A legacy unbound Official row may become the one fixed native-login - // row only when that fixed row is currently absent. No other Codex ID - // change is allowed. - let promotes_legacy_codex_native_login = provider_id_changed - && matches!(app_type, AppType::Codex) - && provider.id == crate::database::CODEX_OFFICIAL_PROVIDER_ID - && provider.category.as_deref() == Some("official") - && Self::managed_codex_oauth_account_id(&provider).is_none() - && existing_provider.as_ref().is_some_and(|existing| { - existing.id != crate::database::CODEX_OFFICIAL_PROVIDER_ID - && existing.category.as_deref() == Some("official") - && Self::managed_codex_oauth_account_id(existing).is_none() - }); - - // Switching the same Official card between native login and a managed - // account changes its storage identity: only the native card owns the - // fixed ID. Keep this exception narrower than general provider renames. - let switches_codex_official_auth_mode = provider_id_changed - && matches!(app_type, AppType::Codex) - && provider.category.as_deref() == Some("official") - && existing_provider.as_ref().is_some_and(|existing| { - if existing.category.as_deref() != Some("official") { - return false; - } - let fixed_id = crate::database::CODEX_OFFICIAL_PROVIDER_ID; - let existing_is_native_id = existing.id == fixed_id; - let target_is_native_id = provider.id == fixed_id; - let existing_has_managed_account = - Self::managed_codex_oauth_account_id(existing).is_some(); - let target_has_managed_account = - Self::managed_codex_oauth_account_id(&provider).is_some(); - - (existing_is_native_id && !target_is_native_id && target_has_managed_account) - || (!existing_is_native_id - && existing_has_managed_account - && target_is_native_id - && !target_has_managed_account) - }); - let replaces_codex_official_identity = - promotes_legacy_codex_native_login || switches_codex_official_auth_mode; - - if switches_codex_official_auth_mode - && state - .db - .get_provider_by_id(&provider.id, app_type.as_str())? - .is_some() - { - return Err(AppError::localized( - "provider.codex.nativeLoginCard.alreadyExists", - "目标登录方式已被另一张 OpenAI Official 卡片使用", - "The target login mode is already used by another OpenAI Official card", - )); - } - - if promotes_legacy_codex_native_login - && state - .db - .get_provider_by_id(&provider.id, app_type.as_str())? - .is_some() - { - return Err(AppError::localized( - "provider.codex.nativeLoginCard.alreadyExists", - "跟随 Codex 登录卡片已存在", - "The Follow Codex login card already exists", - )); - } - - if provider_id_changed - && !promotes_legacy_codex_native_login - && !switches_codex_official_auth_mode - { + if provider_id_changed { if !app_type.is_additive_mode() { return Err(AppError::Message( "Only additive-mode providers support changing provider key".to_string(), @@ -5181,12 +4561,7 @@ impl ProviderService { // For other apps: Check if this is current provider (use effective current, not just DB) let effective_current = crate::settings::get_effective_current_provider(&state.db, &app_type)?; - let current_identity = if replaces_codex_official_identity { - original_id.as_str() - } else { - provider.id.as_str() - }; - let is_current = effective_current.as_deref() == Some(current_identity); + let is_current = effective_current.as_deref() == Some(provider.id.as_str()); let existing_managed_codex_account_id = existing_provider .as_ref() @@ -5197,30 +4572,20 @@ impl ProviderService { existing_provider.as_ref(), &provider, ); - let codex_identity_transaction = matches!(app_type, AppType::Codex) + let managed_codex_update = matches!(app_type, AppType::Codex) && (existing_managed_codex_account_id.is_some() - || target_managed_codex_account_id.is_some() - || promotes_legacy_codex_native_login); + || target_managed_codex_account_id.is_some()); - if codex_identity_transaction { - // A non-current identity update still commits under the same lock: - // once the row is saved, a waiting switch must not observe the old ID - // or binding. + if managed_codex_update { + // A non-current managed row still commits under the same lock: once + // the row is saved, a waiting switch observes the new binding. If we + // released first, a switch could activate the old binding and leave + // DB current/live inconsistent with the subsequent save. if !is_current { - if replaces_codex_official_identity { - state - .db - .replace_provider_id(app_type.as_str(), &original_id, &provider)?; - } else { - state.db.save_provider(app_type.as_str(), &provider)?; - } + state.db.save_provider(app_type.as_str(), &provider)?; return Ok(true); } - let previous_local_current = crate::settings::get_current_provider(&app_type); - let migrates_local_current = replaces_codex_official_identity - && previous_local_current.as_deref() == Some(original_id.as_str()); - let outgoing_live_refresh_token = Self::prepare_outgoing_managed_codex_live_auth( state, outgoing_managed_codex_account_id.as_deref(), @@ -5257,28 +4622,15 @@ impl ProviderService { outgoing_managed_codex_account_id.as_deref(), outgoing_live_refresh_token.as_deref(), )?; - if migrates_local_current { - crate::settings::set_current_provider( - &app_type, - Some(provider.id.as_str()), - )?; - } - if replaces_codex_official_identity { - state - .db - .replace_provider_id(app_type.as_str(), &original_id, &provider)?; - } else { - state.db.save_provider(app_type.as_str(), &provider)?; - } + state.db.save_provider(app_type.as_str(), &provider)?; Ok::<(), AppError>(()) })(); if let Err(error) = commit_result { return Err(Self::managed_codex_transaction_error( - "更新 Codex provider", + "更新托管 Codex provider", error, &snapshot, - migrates_local_current - .then_some((&app_type, previous_local_current.as_deref())), + None, )); } @@ -5340,16 +4692,7 @@ impl ProviderService { // DB is the final commit. Every fallible side effect above can be // restored exactly while the previous provider row is untouched. - if migrates_local_current { - crate::settings::set_current_provider(&app_type, Some(provider.id.as_str()))?; - } - if replaces_codex_official_identity { - state - .db - .replace_provider_id(app_type.as_str(), &original_id, &provider)?; - } else { - state.db.save_provider(app_type.as_str(), &provider)?; - } + state.db.save_provider(app_type.as_str(), &provider)?; Ok::<(), AppError>(()) })(); if let Err(error) = commit_result { @@ -5359,8 +4702,7 @@ impl ProviderService { error, &snapshot, previous_backup.as_ref(), - migrates_local_current - .then_some((&app_type, previous_local_current.as_deref())), + None, )); } @@ -5729,7 +5071,6 @@ impl ProviderService { .as_deref() .and_then(|current_id| providers.get(current_id)) .and_then(Self::managed_codex_oauth_account_id); - let mut backfill_completed = false; if let Some(current_id) = current_id { if current_id != id { @@ -5863,7 +5204,8 @@ impl ProviderService { // failing the switch here would report a switch that in fact happened. if matches!(app_type, AppType::Codex) && backfill_completed - && provider.category.as_deref() == Some("official") + && (provider.category.as_deref() == Some("official") + || crate::proxy::providers::is_codex_official_provider(provider)) && target_managed_codex_account_id.is_none() { let db_auth = provider.settings_config.get("auth"); diff --git a/src-tauri/src/services/proxy.rs b/src-tauri/src/services/proxy.rs index e3d6aeb1e..6e5170ba2 100644 --- a/src-tauri/src/services/proxy.rs +++ b/src-tauri/src/services/proxy.rs @@ -2821,6 +2821,7 @@ impl ProxyService { }; if matches!(app_type_enum, AppType::Codex) { + let is_codex_official = crate::proxy::providers::is_codex_official_provider(provider); let existing_backup_value = self .db .get_live_backup(app_type) @@ -2859,7 +2860,7 @@ impl ProxyService { Self::preserve_codex_auth_in_backup( &mut effective_settings, existing_value, - crate::proxy::providers::is_codex_official_provider(provider), + is_codex_official, )?; } } @@ -2867,12 +2868,16 @@ impl ProxyService { // 统一会话开关:备份是接管释放时恢复 live 的来源,官方配置的 // 共享 custom 路由注入必须落在备份里,否则恢复后开关失效。 crate::codex_config::apply_codex_unified_session_bucket_to_settings( - provider.category.as_deref(), + if is_codex_official { + Some("official") + } else { + provider.category.as_deref() + }, &mut effective_settings, ) .map_err(|e| format!("注入统一会话路由失败: {e}"))?; - if crate::proxy::providers::is_codex_official_provider(provider) { + if is_codex_official { // auth.json keeps rotating independently while takeover is // active. Persisting it in the DB backup would later roll a // valid R1 generation back to the frozen R0 generation. @@ -3068,21 +3073,22 @@ impl ProxyService { } if has_backup && !live_taken_over && matches!(app_type_enum, AppType::Codex) { - let effective_settings = + let effective_provider = build_effective_provider_for_live_with_codex_oauth_manager( self.db.as_ref(), &AppType::Codex, &provider, &self.codex_oauth_manager, ) - .map_err(|e| format!("构建 Codex 有效配置失败: {e}"))? - .settings_config; + .map_err(|e| format!("构建 Codex 有效配置失败: {e}"))?; + let effective_settings = &effective_provider.settings_config; let auth = effective_settings .get("auth") .ok_or_else(|| "Codex 供应商缺少 auth 配置".to_string())?; let config_str = effective_settings.get("config").and_then(|v| v.as_str()); - let profile = - crate::proxy::providers::resolve_codex_catalog_tool_profile(&provider); + let profile = crate::proxy::providers::resolve_codex_catalog_tool_profile( + &effective_provider, + ); if let (Some(account_id), Some(expected_refresh_token)) = ( outgoing_managed_codex_account_id.as_deref(), @@ -3096,8 +3102,8 @@ impl ProxyService { } crate::codex_config::write_codex_provider_live_with_catalog( - &effective_settings, - provider.category.as_deref(), + effective_settings, + effective_provider.category.as_deref(), auth, config_str, profile, @@ -5178,7 +5184,6 @@ wire_api = "responses" json!({ "auth": {}, "config": "model = \"gpt-5.4\"\n" }), None, ); - managed.category = Some("official".to_string()); managed.meta = Some(ProviderMeta { auth_binding: Some(AuthBinding { source: AuthBindingSource::ManagedAccount, diff --git a/src-tauri/tests/provider_service.rs b/src-tauri/tests/provider_service.rs index 5cb09e0f5..602daa73d 100644 --- a/src-tauri/tests/provider_service.rs +++ b/src-tauri/tests/provider_service.rs @@ -834,8 +834,8 @@ requires_openai_auth = true None, ), ); - let mut official_provider = Provider::with_id( - "official-provider".to_string(), + let official_provider = Provider::with_id( + "codex-official".to_string(), "OpenAI Official".to_string(), json!({ "auth": {}, @@ -843,15 +843,14 @@ requires_openai_auth = true }), None, ); - official_provider.category = Some("official".to_string()); manager .providers - .insert("official-provider".to_string(), official_provider); + .insert("codex-official".to_string(), official_provider); } let state = create_test_state_with_config(&initial_config).expect("create test state"); - ProviderService::switch(&state, AppType::Codex, "official-provider") + ProviderService::switch(&state, AppType::Codex, "codex-official") .expect("switch to official provider should succeed without API key"); let auth_value: serde_json::Value = @@ -1062,8 +1061,8 @@ fn reapply_codex_official_live_resyncs_mcp_servers() { let manager = initial_config .get_manager_mut(&AppType::Codex) .expect("codex manager"); - let mut official = Provider::with_id( - "official-provider".to_string(), + let official = Provider::with_id( + "codex-official".to_string(), "Official".to_string(), json!({ "auth": { @@ -1075,10 +1074,9 @@ fn reapply_codex_official_live_resyncs_mcp_servers() { }), None, ); - official.category = Some("official".to_string()); manager .providers - .insert("official-provider".to_string(), official); + .insert("codex-official".to_string(), official); } let servers = initial_config .mcp @@ -1110,7 +1108,7 @@ fn reapply_codex_official_live_resyncs_mcp_servers() { let state = create_test_state_with_config(&initial_config).expect("create test state"); - ProviderService::switch(&state, AppType::Codex, "official-provider") + ProviderService::switch(&state, AppType::Codex, "codex-official") .expect("switch to official provider"); let live = std::fs::read_to_string(cc_switch_lib::get_codex_config_path()) .expect("read config.toml after switch"); diff --git a/src/components/providers/AddProviderDialog.tsx b/src/components/providers/AddProviderDialog.tsx index 465b46fa4..10dc554dd 100644 --- a/src/components/providers/AddProviderDialog.tsx +++ b/src/components/providers/AddProviderDialog.tsx @@ -35,7 +35,6 @@ interface AddProviderDialogProps { providerKey?: string; suggestedDefaults?: OpenClawSuggestedDefaults; ensureClaudeDesktopOfficialSeed?: boolean; - ensureCodexOfficialSeed?: boolean; ensureGrokBuildOfficialSeed?: boolean; }, ) => Promise | void; @@ -165,7 +164,6 @@ export function AddProviderDialog({ providerKey?: string; suggestedDefaults?: OpenClawSuggestedDefaults; ensureClaudeDesktopOfficialSeed?: boolean; - ensureCodexOfficialSeed?: boolean; ensureGrokBuildOfficialSeed?: boolean; } = { name: values.name.trim(), @@ -187,14 +185,6 @@ export function AddProviderDialog({ preset?.category === "official"; } - if (appId === "codex" && values.presetId) { - const presetIndex = parseInt(values.presetId.replace("codex-", "")); - const preset = codexProviderPresets[presetIndex]; - providerData.ensureCodexOfficialSeed = - values.presetCategory === "official" && - preset?.category === "official"; - } - if (appId === "grokbuild" && values.presetId) { providerData.ensureGrokBuildOfficialSeed = values.presetCategory === "official" && diff --git a/src/components/providers/EditProviderDialog.tsx b/src/components/providers/EditProviderDialog.tsx index 20bdef2d2..f73131cc8 100644 --- a/src/components/providers/EditProviderDialog.tsx +++ b/src/components/providers/EditProviderDialog.tsx @@ -16,9 +16,6 @@ import { type AppId, type ManagedAuthProvider, } from "@/lib/api"; -import { resolveManagedAccountId } from "@/lib/authBinding"; -import { CODEX_OFFICIAL_PROVIDER_ID } from "@/utils/providerCapabilities"; -import { generateUUID } from "@/utils/uuid"; interface EditProviderDialogProps { open: boolean; @@ -240,21 +237,11 @@ export function EditProviderDialog({ string, unknown >; - const convertsNativeCodexLoginToManagedAccount = - appId === "codex" && - provider.id === CODEX_OFFICIAL_PROVIDER_ID && - Boolean(resolveManagedAccountId(values.meta, "codex_oauth")?.trim()); const nextProviderId = - appId === "codex" && values.codexNativeLoginSelected - ? CODEX_OFFICIAL_PROVIDER_ID - : convertsNativeCodexLoginToManagedAccount - ? generateUUID() - : (appId === "opencode" || - appId === "openclaw" || - appId === "pi") && - values.providerKey?.trim() - ? values.providerKey.trim() - : provider.id; + (appId === "opencode" || appId === "openclaw" || appId === "pi") && + values.providerKey?.trim() + ? values.providerKey.trim() + : provider.id; const updatedProvider: Provider = { ...provider, diff --git a/src/components/providers/ProviderCard.tsx b/src/components/providers/ProviderCard.tsx index c7ded555e..3927b629c 100644 --- a/src/components/providers/ProviderCard.tsx +++ b/src/components/providers/ProviderCard.tsx @@ -32,7 +32,6 @@ import { } from "@/utils/providerConfigUtils"; import { resolveManagedAccountId } from "@/lib/authBinding"; import { - CODEX_OFFICIAL_PROVIDER_ID, resolveCodexOfficialIdentity, supportsOfficialProxyTakeover, providerNeedsRouting, @@ -301,14 +300,9 @@ export function ProviderCard({ const isHermesReadOnly = appId === "hermes" && isHermesReadOnlyProvider(provider.settingsConfig); const isCodexOauth = - provider.meta?.providerType === PROVIDER_TYPES.CODEX_OAUTH || - isBoundCodexOfficial; - const canDuplicate = !( - appId === "codex" && - (provider.id === CODEX_OFFICIAL_PROVIDER_ID || - (provider.category === "official" && - !resolveManagedAccountId(provider.meta, "codex_oauth")?.trim())) - ); + appId === "codex" + ? isBoundCodexOfficial + : provider.meta?.providerType === PROVIDER_TYPES.CODEX_OAUTH; // xAI OAuth (SuperGrok 反代):额度经自管 OAuth token 自动显示,与 codex_oauth 同构 const isXaiOauth = provider.meta?.providerType === PROVIDER_TYPES.XAI_OAUTH; // 统一权威谓词(详见 providerNeedsRouting):以 providerType 为准,不受 @@ -519,7 +513,7 @@ export function ProviderCard({ )} - {codexOfficialIdentity ? ( + {codexOfficialIdentity && codexOfficialIdentity !== "api_key" ? (
{codexOfficialIdentity === "native_login" ? ( @@ -528,26 +522,6 @@ export function ProviderCard({ defaultValue: "账号会随 Codex CLI 当前登录变化", })} - ) : codexOfficialIdentity === "unbound" ? ( - <> - - - - {t("codex.accountNotSelected", { - defaultValue: "尚未选择账号", - })} - - - - ) : managedCodexAccount ? ( <> onSwitch(provider)} onEdit={() => onEdit(provider)} - onDuplicate={ - canDuplicate ? () => onDuplicate(provider) : undefined - } + onDuplicate={() => onDuplicate(provider)} onTest={ // 连通检测对第三方/自定义/Copilot/Codex-OAuth 供应商开放(这些正是旧的 // 真实请求探测会误报、而可达性探测能正确处理的对象)。官方供应商 diff --git a/src/components/providers/forms/ProviderForm.tsx b/src/components/providers/forms/ProviderForm.tsx index ab2600ec6..a312a8c5e 100644 --- a/src/components/providers/forms/ProviderForm.tsx +++ b/src/components/providers/forms/ProviderForm.tsx @@ -129,7 +129,7 @@ import { HERMES_DEFAULT_CONFIG } from "./hooks/useHermesFormState"; import { resolveManagedAccountId } from "@/lib/authBinding"; import { useOpenClawLiveProviderIds } from "@/hooks/useOpenClaw"; import { useHermesLiveProviderIds } from "@/hooks/useHermes"; -import { CODEX_OFFICIAL_PROVIDER_ID } from "@/utils/providerCapabilities"; +import { resolveCodexOfficialIdentity } from "@/utils/providerCapabilities"; type PresetEntry = { id: string; @@ -306,24 +306,18 @@ function ProviderFormFull({ const { t } = useTranslation(); const isEditMode = Boolean(initialData); - const isCodexNativeLoginProvider = - appId === "codex" && providerId === CODEX_OFFICIAL_PROVIDER_ID; - const startsAsLegacyUnboundCodexOfficial = - appId === "codex" && - isEditMode && - providerId !== CODEX_OFFICIAL_PROVIDER_ID && - initialData?.category === "official" && - !resolveManagedAccountId(initialData.meta, "codex_oauth")?.trim(); - const { data: codexNativeLoginProviderExists = true } = useQuery({ - queryKey: ["providers", "codex", "native-login-exists"], - enabled: appId === "codex" && !isCodexNativeLoginProvider, - queryFn: async () => { - const providers = await providersApi.getAll("codex"); - return Boolean(providers[CODEX_OFFICIAL_PROVIDER_ID]); - }, - }); - const canCreateCodexNativeLoginProvider = - appId === "codex" && !isEditMode && !codexNativeLoginProviderExists; + const initialCodexOfficialIdentity = + appId === "codex" && initialData + ? resolveCodexOfficialIdentity(appId, { + id: providerId ?? "", + category: initialData.category, + meta: initialData.meta, + settingsConfig: initialData.settingsConfig ?? {}, + }) + : null; + const hasExistingCodexOfficialIdentity = + initialCodexOfficialIdentity !== null && + initialCodexOfficialIdentity !== "api_key"; const queryClient = useQueryClient(); const { data: settingsData } = useSettingsQuery(); const showCommonConfigNotice = @@ -389,7 +383,9 @@ function ProviderFormFull({ appId, selectedPresetId, isEditMode, - initialCategory: initialData?.category, + initialCategory: + initialData?.category ?? + (hasExistingCodexOfficialIdentity ? "official" : undefined), }); const isOmoCategory = appId === "opencode" && category === "omo"; const isOmoSlimCategory = appId === "opencode" && category === "omo-slim"; @@ -421,6 +417,7 @@ function ProviderFormFull({ setSelectedCodexAccountId( resolveManagedAccountId(initialData?.meta, "codex_oauth"), ); + setHasValidCodexOfficialSelection(true); setCodexFastMode(initialData?.meta?.codexFastMode ?? false); setCodexChatReasoning(initialData?.meta?.codexChatReasoning ?? {}); setPromptCacheRouting(initialData?.meta?.promptCacheRouting ?? "auto"); @@ -605,10 +602,8 @@ function ProviderFormFull({ const [selectedCodexAccountId, setSelectedCodexAccountId] = useState< string | null >(() => resolveManagedAccountId(initialData?.meta, "codex_oauth")); - const [ - hasExplicitCodexOfficialSelection, - setHasExplicitCodexOfficialSelection, - ] = useState(isEditMode && !startsAsLegacyUnboundCodexOfficial); + const [hasValidCodexOfficialSelection, setHasValidCodexOfficialSelection] = + useState(true); const [selectedXaiAccountId, setSelectedXaiAccountId] = useState< string | null >(() => resolveManagedAccountId(initialData?.meta, "xai_oauth")); @@ -809,23 +804,19 @@ function ProviderFormFull({ const isXaiOauthProvider = (appId === "claude" || appId === "codex") && (presetProviderType === "xai_oauth" || initialProviderType === "xai_oauth"); + const wasCodexOfficialManagedOauthBound = + appId === "codex" && + Boolean(resolveManagedAccountId(initialData?.meta, "codex_oauth")); const isCodexOfficialProvider = appId === "codex" && - (category === "official" || + (hasExistingCodexOfficialIdentity || + wasCodexOfficialManagedOauthBound || (presetProviderType === "codex_oauth" && selectedPresetEntry?.preset.category === "official")); const isCodexOfficialManagedOauthBound = isCodexOfficialProvider && Boolean(selectedCodexAccountId); - const wasCodexOfficialManagedOauthBound = - appId === "codex" && - initialData?.category === "official" && - Boolean(resolveManagedAccountId(initialData?.meta, "codex_oauth")); - const canSelectCodexNativeLogin = - isCodexNativeLoginProvider || - canCreateCodexNativeLoginProvider || - (isEditMode && isCodexOfficialProvider && !codexNativeLoginProviderExists); const requiresExplicitCodexOfficialSelection = - isCodexOfficialProvider && !hasExplicitCodexOfficialSelection; + isCodexOfficialProvider && !hasValidCodexOfficialSelection; const requiresCodexOauthLogin = isClaudeCodexOauthProvider || isCodexOfficialManagedOauthBound; @@ -1510,17 +1501,16 @@ function ProviderFormFull({ if (appId === "codex") { try { - const shouldStripManagedCodexAuth = - category === "official" && - (isCodexOfficialManagedOauthBound || - wasCodexOfficialManagedOauthBound); - const authJson = shouldStripManagedCodexAuth + const shouldStripCodexOfficialAuth = + isCodexOfficialManagedOauthBound || wasCodexOfficialManagedOauthBound; + const authJson = shouldStripCodexOfficialAuth ? {} : JSON.parse(codexAuth); + const codexConfigForSave = codexConfig ?? ""; let normalizedCodexConfig = - category !== "official" && (codexConfig ?? "").trim() - ? setCodexWireApi(codexConfig ?? "", "responses") - : (codexConfig ?? ""); + category !== "official" && codexConfigForSave.trim() + ? setCodexWireApi(codexConfigForSave, "responses") + : codexConfigForSave; // 模型映射与「路由接管」解耦:对所有非官方供应商,填了就持久化 //(Chat 生成兼容路由、原生 Responses 生成 model-catalogs.json), // 留空归一化为 [] 即不写。后端只看 modelCatalog.models 是否非空。 @@ -1603,10 +1593,7 @@ function ProviderFormFull({ }; if (isCodexOfficialProvider) { - payload.codexNativeLoginSelected = - !selectedCodexAccountId && - hasExplicitCodexOfficialSelection && - canSelectCodexNativeLogin; + payload.presetCategory = "official"; } if (appId === "opencode") { @@ -2471,10 +2458,10 @@ function ProviderFormFull({ selectedCodexAccountId={selectedCodexAccountId} onCodexAccountSelect={setSelectedCodexAccountId} onCodexAuthSelectionConfirmed={() => - setHasExplicitCodexOfficialSelection(true) + setHasValidCodexOfficialSelection(true) } onCodexAuthSelectionInvalidated={() => - setHasExplicitCodexOfficialSelection(false) + setHasValidCodexOfficialSelection(false) } onManageAuthAccounts={onManageAuthAccounts} codexOauthSelectionLabel={t("codexOauth.signInMethod")} @@ -2482,10 +2469,8 @@ function ProviderFormFull({ codexOauthNoneOptionDescription={t( "codex.followCodexLoginDescription", )} - codexOauthAllowUnboundSelection={canSelectCodexNativeLogin} - codexOauthAllowUnboundSelectionWithoutStatus={ - canSelectCodexNativeLogin - } + codexOauthAllowUnboundSelection + codexOauthAllowUnboundSelectionWithoutStatus codexOauthRequireExplicitSelection={ requiresExplicitCodexOfficialSelection } @@ -2886,6 +2871,4 @@ export type ProviderFormValues = ProviderFormData & { meta?: ProviderMeta; providerKey?: string; // OpenCode/OpenClaw: user-defined provider key suggestedDefaults?: OpenClawSuggestedDefaults; // OpenClaw: suggested default model configuration - /** Transient UI intent used to promote a legacy Codex Official row. */ - codexNativeLoginSelected?: boolean; }; diff --git a/src/hooks/useProviderActions.ts b/src/hooks/useProviderActions.ts index 818107143..181628a86 100644 --- a/src/hooks/useProviderActions.ts +++ b/src/hooks/useProviderActions.ts @@ -89,7 +89,6 @@ export function useProviderActions( suggestedDefaults?: OpenClawSuggestedDefaults; addToLive?: boolean; ensureClaudeDesktopOfficialSeed?: boolean; - ensureCodexOfficialSeed?: boolean; ensureGrokBuildOfficialSeed?: boolean; }, ) => { diff --git a/src/lib/query/mutations.ts b/src/lib/query/mutations.ts index 9aea09331..2958ed492 100644 --- a/src/lib/query/mutations.ts +++ b/src/lib/query/mutations.ts @@ -15,11 +15,7 @@ import { invalidateHermesProviderCaches } from "@/hooks/useHermes"; import { proxyKeys } from "@/lib/query/proxy"; import { usageKeys } from "@/lib/query/usage"; import { invalidatePiProviderCaches } from "@/lib/query/pi"; -import { resolveManagedAccountId } from "@/lib/authBinding"; -import { - CODEX_OFFICIAL_PROVIDER_ID, - GROKBUILD_OFFICIAL_PROVIDER_ID, -} from "@/utils/providerCapabilities"; +import { GROKBUILD_OFFICIAL_PROVIDER_ID } from "@/utils/providerCapabilities"; export const useAddProviderMutation = (appId: AppId) => { const queryClient = useQueryClient(); @@ -31,7 +27,6 @@ export const useAddProviderMutation = (appId: AppId) => { providerKey?: string; addToLive?: boolean; ensureClaudeDesktopOfficialSeed?: boolean; - ensureCodexOfficialSeed?: boolean; ensureGrokBuildOfficialSeed?: boolean; }, ) => { @@ -39,7 +34,6 @@ export const useAddProviderMutation = (appId: AppId) => { providerKey: _providerKey, addToLive, ensureClaudeDesktopOfficialSeed, - ensureCodexOfficialSeed, ensureGrokBuildOfficialSeed, ...rest } = providerInput; @@ -54,34 +48,6 @@ export const useAddProviderMutation = (appId: AppId) => { return officialProvider; } - if (appId === "codex" && ensureCodexOfficialSeed) { - // The fixed seed is the one native "Codex current login" card. - // A managed account gets its own provider row so several accounts can - // coexist without replacing that native-login entry. - const managedAccountId = resolveManagedAccountId( - rest.meta, - "codex_oauth", - )?.trim(); - if (!managedAccountId) { - await providersApi.ensureCodexOfficialProvider(); - const providers = await providersApi.getAll(appId); - const nativeLoginProvider = providers[CODEX_OFFICIAL_PROVIDER_ID]; - if (!nativeLoginProvider) { - throw new Error("Codex current-login provider was not created"); - } - return nativeLoginProvider; - } - - const managedOfficialProvider: Provider = { - ...rest, - id: generateUUID(), - category: "official", - createdAt: Date.now(), - }; - await providersApi.add(managedOfficialProvider, appId, addToLive); - return managedOfficialProvider; - } - if (appId === "grokbuild" && ensureGrokBuildOfficialSeed) { await providersApi.ensureGrokBuildOfficialProvider(); const providers = await providersApi.getAll(appId); diff --git a/src/utils/providerCapabilities.test.ts b/src/utils/providerCapabilities.test.ts index 44cd8e5dd..5c63cb5e9 100644 --- a/src/utils/providerCapabilities.test.ts +++ b/src/utils/providerCapabilities.test.ts @@ -16,11 +16,16 @@ const codexConfig = (wireApi: "chat_completions" | "responses") => `model_provider = "custom"\n\n[model_providers.custom]\nname = "X"\nbase_url = "https://x.example/v1"\nwire_api = "${wireApi}"\n`; describe("providerNeedsRouting", () => { - it("allows only native-login and managed Codex Official cards during takeover", () => { - const native = mkProvider({ id: "codex-official", category: "official" }); + it("allows explicit Codex Official cards during takeover", () => { + const native = mkProvider({ + id: "codex-official", + category: "official", + settingsConfig: { auth: {}, config: "" }, + }); const managed = mkProvider({ id: "managed-account-card", category: "official", + settingsConfig: { auth: {}, config: "" }, meta: { authBinding: { source: "managed_account", @@ -30,20 +35,157 @@ describe("providerNeedsRouting", () => { }, }); const unbound = mkProvider({ - id: "legacy-unbound", + id: "follow-login", category: "official", + settingsConfig: { + auth: { + auth_mode: "chatgpt", + tokens: { refresh_token: "legacy-live-only-token" }, + }, + config: "", + }, + }); + const fixedManaged = mkProvider({ + id: "codex-official", + category: "official", + settingsConfig: { auth: {}, config: "" }, + meta: { + providerType: "codex_oauth", + authBinding: { + source: "managed_account", + authProvider: "codex_oauth", + accountId: "acct-fixed-managed", + }, + }, }); expect(resolveCodexOfficialIdentity("codex", native)).toBe("native_login"); expect(resolveCodexOfficialIdentity("codex", managed)).toBe( "managed_account", ); - expect(resolveCodexOfficialIdentity("codex", unbound)).toBe("unbound"); + expect(resolveCodexOfficialIdentity("codex", unbound)).toBe("native_login"); + expect(resolveCodexOfficialIdentity("codex", fixedManaged)).toBe( + "managed_account", + ); expect(resolveCodexOfficialIdentity("claude", managed)).toBeNull(); expect(supportsOfficialProxyTakeover("codex", native)).toBe(true); expect(supportsOfficialProxyTakeover("codex", managed)).toBe(true); - expect(supportsOfficialProxyTakeover("codex", unbound)).toBe(false); + expect(supportsOfficialProxyTakeover("codex", unbound)).toBe(true); + }); + + it("does not infer Codex login identity from a stale Official category", () => { + const storedAuthKey = mkProvider({ + id: "legacy-api-key", + category: "official", + settingsConfig: { + auth: { OPENAI_API_KEY: "sk-legacy" }, + config: "", + }, + }); + const storedBearer = mkProvider({ + id: "legacy-bearer", + category: "official", + settingsConfig: { + auth: {}, + config: 'experimental_bearer_token = "sk-legacy"', + }, + }); + const explicitOpenAi = mkProvider({ + id: "official-openai", + category: "official", + settingsConfig: { + auth: { OPENAI_API_KEY: "sk-official" }, + config: 'model_provider = "openai"', + }, + }); + const grokOfficial = mkProvider({ + id: "grokbuild-official", + category: "official", + settingsConfig: { config: "" }, + }); + const unmarkedCustom = mkProvider({ + id: "custom-upstream", + category: "official", + settingsConfig: { + auth: {}, + config: + 'model_provider = "custom"\n[model_providers.custom]\nbase_url = "https://example.com/v1"', + }, + }); + const implicitCustom = mkProvider({ + id: "implicit-custom-upstream", + category: "official", + settingsConfig: { + auth: {}, + config: 'model_provider = "ollama"', + }, + }); + expect(resolveCodexOfficialIdentity("codex", storedAuthKey)).toBe( + "api_key", + ); + expect(supportsOfficialProxyTakeover("codex", storedAuthKey)).toBe(false); + expect(resolveCodexOfficialIdentity("codex", storedBearer)).toBeNull(); + expect(resolveCodexOfficialIdentity("codex", explicitOpenAi)).toBe( + "api_key", + ); + expect(resolveCodexOfficialIdentity("codex", grokOfficial)).toBeNull(); + expect(resolveCodexOfficialIdentity("codex", unmarkedCustom)).toBeNull(); + expect(resolveCodexOfficialIdentity("codex", implicitCustom)).toBeNull(); + + const unifiedSession = mkProvider({ + id: "unified-session", + category: "official", + settingsConfig: { + auth: {}, + config: + 'model_provider = "custom"\n[model_providers.custom]\nname = "OpenAI"\nrequires_openai_auth = true\nsupports_websockets = true\nwire_api = "responses"', + }, + }); + expect(resolveCodexOfficialIdentity("codex", unifiedSession)).toBe( + "native_login", + ); + }); + + it("keeps category-less fixed and managed legacy cards recognizable", () => { + const fixed = mkProvider({ + id: "codex-official", + settingsConfig: { auth: {}, config: "" }, + }); + const fixedApiKey = mkProvider({ + id: "codex-official", + settingsConfig: { + auth: { OPENAI_API_KEY: "sk-legacy" }, + config: "", + }, + }); + const fixedCustom = mkProvider({ + id: "codex-official", + settingsConfig: { + auth: {}, + config: 'model_provider = "custom"', + }, + }); + const managed = mkProvider({ + id: "legacy-managed", + settingsConfig: { auth: {}, config: null }, + meta: { + authBinding: { + source: "managed_account", + authProvider: "codex_oauth", + accountId: "acct-managed", + }, + }, + }); + + expect(resolveCodexOfficialIdentity("codex", fixed)).toBe("native_login"); + expect(resolveCodexOfficialIdentity("codex", managed)).toBe( + "managed_account", + ); + expect(resolveCodexOfficialIdentity("codex", fixedApiKey)).toBeNull(); + expect(resolveCodexOfficialIdentity("codex", fixedCustom)).toBeNull(); + expect(providerNeedsRouting("codex", fixed)).toBe(false); + expect(providerNeedsRouting("codex", managed)).toBe(false); }); it("官方供应商一律不需要路由(即便 providerType 是 OAuth)", () => { diff --git a/src/utils/providerCapabilities.ts b/src/utils/providerCapabilities.ts index b66245921..4bf0e16f7 100644 --- a/src/utils/providerCapabilities.ts +++ b/src/utils/providerCapabilities.ts @@ -3,7 +3,10 @@ import type { Provider } from "@/types"; import { isOAuthProviderType } from "@/config/constants"; import { resolveManagedAccountId } from "@/lib/authBinding"; import { + extractCodexBaseUrl, + extractCodexExperimentalBearerToken, extractCodexWireApi, + hasExplicitNonOpenAiCodexModelProvider, isCodexAnthropicWireApi, isCodexChatWireApi, } from "@/utils/providerConfigUtils"; @@ -14,27 +17,87 @@ export const GROKBUILD_OFFICIAL_PROVIDER_ID = "grokbuild-official"; export type CodexOfficialIdentity = | "native_login" | "managed_account" - | "unbound"; + | "api_key"; + +const nonEmptyString = (value: unknown): boolean => + typeof value === "string" && value.trim().length > 0; + +function hasExplicitCodexThirdPartyUpstream( + settings: Record, +): boolean { + const config = typeof settings.config === "string" ? settings.config : ""; + + return ( + nonEmptyString(settings.baseUrl) || + nonEmptyString(settings.baseURL) || + nonEmptyString(settings.base_url) || + Boolean(extractCodexExperimentalBearerToken(config)) || + Boolean(extractCodexBaseUrl(config)) || + hasExplicitNonOpenAiCodexModelProvider(config) + ); +} + +function hasStoredCodexApiKey(settings: Record): boolean { + const auth = settings.auth as Record | undefined; + return nonEmptyString(auth?.OPENAI_API_KEY); +} export function resolveCodexOfficialIdentity( appId: AppId, - provider: Pick, + provider: Pick, ): CodexOfficialIdentity | null { - if (appId !== "codex" || provider.category !== "official") return null; - if (provider.id === CODEX_OFFICIAL_PROVIDER_ID) return "native_login"; - if (resolveManagedAccountId(provider.meta, "codex_oauth")?.trim()) { + if (appId !== "codex") return null; + + const managedAccountId = resolveManagedAccountId( + provider.meta, + "codex_oauth", + )?.trim(); + const hasFixedOfficialId = provider.id === CODEX_OFFICIAL_PROVIDER_ID; + if (hasFixedOfficialId && provider.category === "official") { + return managedAccountId ? "managed_account" : "native_login"; + } + + const settings = provider.settingsConfig as Record; + const auth = settings?.auth; + const config = settings?.config; + if ( + !auth || + typeof auth !== "object" || + Array.isArray(auth) || + (config != null && typeof config !== "string") + ) { + return null; + } + + if (hasExplicitCodexThirdPartyUpstream(settings)) { + return null; + } + + if (managedAccountId) { return "managed_account"; } - return "unbound"; + if (hasStoredCodexApiKey(settings)) { + return provider.category === "official" ? "api_key" : null; + } + return hasFixedOfficialId || provider.category === "official" + ? "native_login" + : null; } /** Keep the UI capability rule aligned with the Rust takeover policy. */ export function supportsOfficialProxyTakeover( appId: AppId, - provider: Pick, + provider: Pick, ): boolean { const identity = resolveCodexOfficialIdentity(appId, provider); - return identity === "native_login" || identity === "managed_account"; + if (!identity || identity === "api_key") return false; + if ( + provider.id === CODEX_OFFICIAL_PROVIDER_ID || + identity === "managed_account" + ) { + return true; + } + return true; } /** @@ -54,7 +117,11 @@ export function providerNeedsRouting( appId: AppId, provider: Provider, ): boolean { - if (provider.category === "official") return false; + if ( + provider.category === "official" || + resolveCodexOfficialIdentity(appId, provider) + ) + return false; const isManagedOAuth = isOAuthProviderType(provider.meta?.providerType); diff --git a/src/utils/providerConfigUtils.ts b/src/utils/providerConfigUtils.ts index 0f90cf7e2..5d59453c6 100644 --- a/src/utils/providerConfigUtils.ts +++ b/src/utils/providerConfigUtils.ts @@ -566,6 +566,38 @@ const getCodexModelProviderName = (configText: string): string | undefined => { return providerName || undefined; }; +const isCodexUnifiedSessionProjection = (configText: string): boolean => { + try { + const parsed = parseToml(normalizeTomlText(configText)) as Record< + string, + any + >; + const custom = parsed.model_providers?.custom; + return ( + parsed.model_provider === "custom" && + isPlainObject(custom) && + Object.keys(custom).length === 4 && + custom.name === "OpenAI" && + custom.requires_openai_auth === true && + custom.supports_websockets === true && + custom.wire_api === "responses" + ); + } catch { + return false; + } +}; + +export const hasExplicitNonOpenAiCodexModelProvider = ( + configText: string | undefined | null, +): boolean => { + if (typeof configText !== "string") return false; + if (isCodexUnifiedSessionProjection(configText)) return false; + const providerName = getCodexModelProviderName(configText); + return Boolean( + providerName && providerName.trim().toLowerCase() !== "openai", + ); +}; + const getCodexProviderSectionName = ( configText: string, ): string | undefined => { diff --git a/tests/components/AddProviderDialog.test.tsx b/tests/components/AddProviderDialog.test.tsx index dedccc384..6b7c21806 100644 --- a/tests/components/AddProviderDialog.test.tsx +++ b/tests/components/AddProviderDialog.test.tsx @@ -162,7 +162,7 @@ describe("AddProviderDialog", () => { }); }); - it("submits the managed account selected from the Codex Official preset", async () => { + it("submits the optional managed account from the Codex Official preset", async () => { const handleSubmit = vi.fn().mockResolvedValue(undefined); const officialPresetIndex = codexProviderPresets.findIndex( (preset) => @@ -201,7 +201,6 @@ describe("AddProviderDialog", () => { expect(handleSubmit).toHaveBeenCalledWith( expect.objectContaining({ category: "official", - ensureCodexOfficialSeed: true, meta: expect.objectContaining({ authBinding: { source: "managed_account", @@ -211,6 +210,9 @@ describe("AddProviderDialog", () => { }), }), ); + expect(handleSubmit.mock.calls[0][0]).not.toHaveProperty( + "ensureCodexOfficialSeed", + ); }); it("clears the nested auth panel before the dialog reopens", async () => { diff --git a/tests/components/EditProviderDialog.test.tsx b/tests/components/EditProviderDialog.test.tsx index 87c41c034..f631e6268 100644 --- a/tests/components/EditProviderDialog.test.tsx +++ b/tests/components/EditProviderDialog.test.tsx @@ -15,7 +15,6 @@ const apiMocks = vi.hoisted(() => ({ getOpenClawLiveProvider: vi.fn(), })); let mockFormReady = true; -let mockCodexNativeLoginSelected = false; let mockCodexManagedAccountSelected = false; let submitReadyCallbacks: Array<(isReady: boolean) => void> = []; @@ -74,7 +73,6 @@ vi.mock("@/components/providers/forms/ProviderForm", () => ({ meta?: Record; icon?: string; iconColor?: string; - codexNativeLoginSelected?: boolean; }) => void; onSubmitReadyChange?: (isReady: boolean) => void; onManageAuthAccounts?: (target: "codex_oauth") => void; @@ -110,7 +108,6 @@ vi.mock("@/components/providers/forms/ProviderForm", () => ({ : initialData.meta, icon: initialData.icon, iconColor: initialData.iconColor, - codexNativeLoginSelected: mockCodexNativeLoginSelected, }); }} > @@ -141,7 +138,6 @@ import { EditProviderDialog } from "@/components/providers/EditProviderDialog"; describe("EditProviderDialog", () => { beforeEach(() => { mockFormReady = true; - mockCodexNativeLoginSelected = false; mockCodexManagedAccountSelected = false; submitReadyCallbacks = []; apiMocks.getCurrent.mockReset(); @@ -283,8 +279,7 @@ describe("EditProviderDialog", () => { }); }); - it("promotes a legacy Codex Official row after native login is selected", async () => { - mockCodexNativeLoginSelected = true; + it("keeps an unbound Codex Official provider ID unchanged", async () => { apiMocks.getCurrent.mockResolvedValue(null); const onSubmit = vi.fn(); const provider: Provider = { @@ -310,12 +305,12 @@ describe("EditProviderDialog", () => { expect(onSubmit).toHaveBeenCalledWith( expect.objectContaining({ originalId: "legacy-unbound-official", - provider: expect.objectContaining({ id: "codex-official" }), + provider: expect.objectContaining({ id: "legacy-unbound-official" }), }), ); }); - it("moves the fixed Codex card to a managed-account ID when its login changes", async () => { + it("keeps the fixed Codex provider ID when an account is bound", async () => { mockCodexManagedAccountSelected = true; apiMocks.getCurrent.mockResolvedValue(null); const onSubmit = vi.fn(); @@ -341,10 +336,7 @@ describe("EditProviderDialog", () => { await waitFor(() => expect(onSubmit).toHaveBeenCalledTimes(1)); const submitted = onSubmit.mock.calls[0][0]; expect(submitted.originalId).toBe("codex-official"); - expect(submitted.provider.id).not.toBe("codex-official"); - expect(submitted.provider.id).toMatch( - /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i, - ); + expect(submitted.provider.id).toBe("codex-official"); expect(submitted.provider.meta?.authBinding).toEqual({ source: "managed_account", authProvider: "codex_oauth", diff --git a/tests/components/ProviderCard.codexAccount.test.tsx b/tests/components/ProviderCard.codexAccount.test.tsx index 12a72f54f..71fdda832 100644 --- a/tests/components/ProviderCard.codexAccount.test.tsx +++ b/tests/components/ProviderCard.codexAccount.test.tsx @@ -10,11 +10,19 @@ import { createTestQueryClient } from "../utils/testQueryClient"; const codexQuotaFooterProps = vi.hoisted(() => vi.fn()); vi.mock("@/components/providers/ProviderActions", () => ({ - ProviderActions: (props: { onConfigureUsage?: () => void }) => { - return props.onConfigureUsage ? ( - - ) : null; - }, + ProviderActions: (props: { + onDuplicate?: () => void; + onConfigureUsage?: () => void; + }) => ( + <> + {props.onDuplicate ? ( + + ) : null} + {props.onConfigureUsage ? ( + + ) : null} + + ), })); vi.mock("@/components/ProviderIcon", () => ({ @@ -29,7 +37,7 @@ vi.mock("@/components/CopilotQuotaFooter", () => ({ default: () => null })); vi.mock("@/components/CodexOauthQuotaFooter", () => ({ default: (props: unknown) => { codexQuotaFooterProps(props); - return null; + return
codex-oauth-quota
; }, })); vi.mock("@/components/XaiOauthQuotaFooter", () => ({ default: () => null })); @@ -49,7 +57,7 @@ const managedProvider = ( id: "managed-official", name, category: "official", - settingsConfig: {}, + settingsConfig: { auth: {}, config: "" }, meta: { providerType: "codex_oauth", authBinding: { @@ -225,7 +233,7 @@ describe("ProviderCard Codex Official account identity", () => { id: "codex-official", name: "OpenAI Official", category: "official", - settingsConfig: {}, + settingsConfig: { auth: {}, config: "" }, }); expect( @@ -234,6 +242,24 @@ describe("ProviderCard Codex Official account identity", () => { expect( screen.getByText("账号会随 Codex CLI 当前登录变化"), ).toBeInTheDocument(); + expect(screen.queryByText("codex-oauth-quota")).not.toBeInTheDocument(); + }); + + it("shows the managed account when it is bound to the fixed card", () => { + const login = "fixed@example.com"; + renderCard( + { + ...managedProvider("OpenAI Official"), + id: "codex-official", + }, + { status: authStatus(login) }, + ); + + expect(screen.getByText(login)).toBeInTheDocument(); + expect( + screen.queryByText("账号会随 Codex CLI 当前登录变化"), + ).not.toBeInTheDocument(); + expect(screen.getByText("codex-oauth-quota")).toBeInTheDocument(); }); it("shows a manual note instead of generated account guidance", () => { @@ -242,7 +268,7 @@ describe("ProviderCard Codex Official account identity", () => { name: "OpenAI Official", notes: "Primary work provider", category: "official", - settingsConfig: {}, + settingsConfig: { auth: {}, config: "" }, }); expect(screen.getByText("Primary work provider")).toBeInTheDocument(); @@ -251,36 +277,42 @@ describe("ProviderCard Codex Official account identity", () => { ).not.toBeInTheDocument(); }); - it("makes a legacy unbound card actionable without changing it", async () => { - const user = userEvent.setup(); - const onEdit = vi.fn(); + it("treats a legacy unbound Official card as follow-login", () => { const provider: Provider = { id: "legacy-unbound", name: "Legacy Official", category: "official", - settingsConfig: {}, - meta: { - providerType: "codex_oauth", - usage_script: { - enabled: true, - language: "javascript", - code: "", - autoQueryInterval: 0, - }, - }, + settingsConfig: { auth: {}, config: "" }, + meta: { providerType: "codex_oauth" }, }; - renderCard(provider, { isCurrent: true, onEdit }); + renderCard(provider, { isCurrent: true }); - expect(codexQuotaFooterProps).toHaveBeenCalledWith( - expect.objectContaining({ autoQueryInterval: undefined }), - ); - expect(screen.getByText("尚未选择账号").parentElement).toHaveClass( - "text-sm", - ); - const chooseAccount = screen.getByRole("button", { name: "选择账号" }); - expect(chooseAccount).toHaveClass("text-sm"); - expect(screen.queryByText("使用中")).not.toBeInTheDocument(); - await user.click(chooseAccount); - expect(onEdit).toHaveBeenCalledWith(provider); +expect( + screen.getByText("账号会随 Codex CLI 当前登录变化"), + ).toBeInTheDocument(); + expect( + screen.queryByRole("button", { name: "选择账号" }), + ).not.toBeInTheDocument(); + expect( + screen.getByRole("button", { name: "duplicate-provider" }), + ).toBeInTheDocument(); + expect(screen.queryByText("codex-oauth-quota")).not.toBeInTheDocument(); + expect(codexQuotaFooterProps).not.toHaveBeenCalled(); + }); + + it("does not label a stored API-key card as follow-login", () => { + renderCard({ + id: "legacy-api-key", + name: "Legacy API Key", + category: "official", + settingsConfig: { + auth: { OPENAI_API_KEY: "sk-stored" }, + config: "", + }, + }); + + expect( + screen.queryByText("账号会随 Codex CLI 当前登录变化"), + ).not.toBeInTheDocument(); }); }); diff --git a/tests/components/ProviderForm.codexManagedAccount.test.tsx b/tests/components/ProviderForm.codexManagedAccount.test.tsx index 6c9c02276..db85c3b3b 100644 --- a/tests/components/ProviderForm.codexManagedAccount.test.tsx +++ b/tests/components/ProviderForm.codexManagedAccount.test.tsx @@ -13,9 +13,6 @@ const authState = vi.hoisted(() => ({ const toastMocks = vi.hoisted(() => ({ error: vi.fn(), })); -const providerApiMocks = vi.hoisted(() => ({ - getAll: vi.fn(), -})); vi.mock("sonner", () => ({ toast: { @@ -24,43 +21,29 @@ vi.mock("sonner", () => ({ }, })); -vi.mock("@/lib/api", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - providersApi: { - ...actual.providersApi, - getAll: (...args: unknown[]) => providerApiMocks.getAll(...args), - }, - }; -}); - vi.mock("@/components/providers/forms/CodexOAuthSection", () => ({ CodexOAuthSection: ({ onAccountSelect, onSelectionConfirmed, onSelectionInvalidated, allowUnboundSelection = true, - nativeLoginOnly = false, - requireExplicitSelection = false, + allowUnboundSelectionWithoutStatus = false, }: { onAccountSelect?: (accountId: string | null) => void; onSelectionConfirmed?: () => void; onSelectionInvalidated?: () => void; allowUnboundSelection?: boolean; - nativeLoginOnly?: boolean; - requireExplicitSelection?: boolean; + allowUnboundSelectionWithoutStatus?: boolean; }) => (
- - {nativeLoginOnly ? "true" : "false"} + + {allowUnboundSelection ? "true" : "false"} - - {requireExplicitSelection ? "true" : "false"} + + {allowUnboundSelectionWithoutStatus ? "true" : "false"}