diff --git a/src-tauri/src/agents/commands.rs b/src-tauri/src/agents/commands.rs index 85bc00e..9d4803b 100644 --- a/src-tauri/src/agents/commands.rs +++ b/src-tauri/src/agents/commands.rs @@ -1406,6 +1406,9 @@ pub(crate) fn mark_configured_agent_model_aliases( let Some(provider) = provider.as_mapping() else { continue; }; + if yaml_mapping_value(provider, "disabled") == Some(&serde_norway::Value::Bool(true)) { + continue; + } let Some(configured_models) = yaml_mapping_value(provider, "models") else { continue; }; diff --git a/src-tauri/src/core_config/aliases.rs b/src-tauri/src/core_config/aliases.rs index 0335d75..e760c05 100644 --- a/src-tauri/src/core_config/aliases.rs +++ b/src-tauri/src/core_config/aliases.rs @@ -528,20 +528,29 @@ pub(crate) fn is_managed_claude_model_alias(model: &serde_norway::Value, alias: .is_some_and(|value| value == expected_display_name) } -pub(crate) fn configured_managed_claude_alias_exists( +pub(crate) fn configured_managed_claude_alias_matches( root: &serde_norway::Mapping, alias: &str, + source_model: &str, ) -> bool { + let matches = |model: &serde_norway::Value| { + is_managed_claude_model_alias(model, alias) + && configured_model_identity(model) + .is_some_and(|(source, _, _)| source.eq_ignore_ascii_case(source_model)) + }; let configured_provider_alias = MODEL_ALIAS_CONFIG_SECTIONS.iter().any(|section| { yaml_mapping_value(root, section) .and_then(serde_norway::Value::as_sequence) .into_iter() .flatten() .filter_map(serde_norway::Value::as_mapping) + .filter(|provider| { + yaml_mapping_value(provider, "disabled") != Some(&serde_norway::Value::Bool(true)) + }) .filter_map(|provider| yaml_mapping_value(provider, "models")) .filter_map(serde_norway::Value::as_sequence) .flatten() - .any(|model| is_managed_claude_model_alias(model, alias)) + .any(matches) }); configured_provider_alias || yaml_mapping_value(root, "oauth-model-alias") @@ -550,7 +559,7 @@ pub(crate) fn configured_managed_claude_alias_exists( .flat_map(|channels| channels.values()) .filter_map(serde_norway::Value::as_sequence) .flatten() - .any(|model| is_managed_claude_model_alias(model, alias)) + .any(matches) } pub(crate) fn ensure_claude_desktop_model_alias( @@ -559,14 +568,10 @@ pub(crate) fn ensure_claude_desktop_model_alias( alias: &str, oauth_model_definitions: &[OAuthModelDefinitions], ) -> Result<(), String> { - if let Some((existing_source, _)) = configured_model_client_identity(root, alias) { - if existing_source.eq_ignore_ascii_case(source_model) - && configured_managed_claude_alias_exists(root, alias) - { - return Ok(()); - } - remove_existing_claude_model_alias(root, alias)?; + if configured_managed_claude_alias_matches(root, alias, source_model) { + return Ok(()); } + remove_existing_claude_model_alias(root, alias)?; if append_claude_desktop_model_alias(root, source_model, alias)? { return Ok(()); } @@ -657,6 +662,9 @@ pub(crate) fn append_claude_desktop_model_alias( let Some(provider) = provider.as_mapping_mut() else { continue; }; + if yaml_mapping_value(provider, "disabled") == Some(&serde_norway::Value::Bool(true)) { + continue; + } let Some(models) = yaml_mapping_value_mut(provider, "models") else { continue; }; diff --git a/src-tauri/src/tests.rs b/src-tauri/src/tests.rs index 2a5e902..0921fb9 100644 --- a/src-tauri/src/tests.rs +++ b/src-tauri/src/tests.rs @@ -12,6 +12,7 @@ mod app_settings; mod app_update; mod core_config; mod core_runtime; +mod desktop_alias_routing; #[cfg(windows)] mod file_replace; mod instance_lock; diff --git a/src-tauri/src/tests/desktop_alias_routing.rs b/src-tauri/src/tests/desktop_alias_routing.rs new file mode 100644 index 0000000..ebd9318 --- /dev/null +++ b/src-tauri/src/tests/desktop_alias_routing.rs @@ -0,0 +1,84 @@ +use super::support::*; +use super::*; + +fn json(content: &str) -> serde_json::Value { + serde_json::to_value(serde_norway::from_str::(content).unwrap()).unwrap() +} + +#[test] +fn desktop_routes_move_from_disabled_provider_to_enabled_source() { + let models = test_agent_models(&["grok-4.6"]); + let mappings = ClaudeDesktopModelMappings::all("grok-4.6"); + let initial = + "openai-compatibility:\n - name: disabled-provider\n models: [{name: grok-4.6}]\n"; + let configured = + ensure_claude_desktop_model_aliases_in_yaml(initial, &mappings, &models).unwrap(); + let disabled = configured.replacen( + "name: disabled-provider", + "name: disabled-provider\n disabled: true", + 1, + ); + let input = format!("{disabled}codex-api-key:\n - models: [{{name: grok-4.6}}]\n"); + let restored = ensure_claude_desktop_model_aliases_in_yaml(&input, &mappings, &models).unwrap(); + let after = json(&restored); + assert_eq!(after["openai-compatibility"][0]["disabled"], true); + assert_eq!( + after["openai-compatibility"][0]["models"], + serde_json::json!([{"name": "grok-4.6"}]) + ); + let active_models = after["codex-api-key"][0]["models"].as_array().unwrap(); + for route in [ + CLAUDE_DESKTOP_OPUS_MODEL_ID, + CLAUDE_DESKTOP_SONNET_MODEL_ID, + CLAUDE_DESKTOP_HAIKU_MODEL_ID, + ] { + assert!(active_models + .iter() + .any(|model| model["name"] == "grok-4.6" && model["alias"] == route)); + } + let repeated = + ensure_claude_desktop_model_aliases_in_yaml(&restored, &mappings, &models).unwrap(); + assert_eq!(json(&repeated), after); +} + +#[test] +fn desktop_routes_skip_disabled_sources_during_creation() { + let input = "openai-compatibility:\n - name: disabled-provider\n disabled: true\n models: [{name: model-a}]\n - name: enabled-provider\n models: [{name: model-a}]\n"; + let updated = ensure_claude_desktop_model_aliases_in_yaml( + input, + &ClaudeDesktopModelMappings::all("model-a"), + &test_agent_models(&["model-a"]), + ) + .unwrap(); + let after = json(&updated); + assert_eq!( + after["openai-compatibility"][0], + json(input)["openai-compatibility"][0] + ); + assert_eq!( + after["openai-compatibility"][1]["models"] + .as_array() + .unwrap() + .len(), + 4 + ); +} + +#[test] +fn desktop_routes_reject_sources_available_only_in_disabled_providers() { + let input = "openai-compatibility:\n - name: disabled-provider\n disabled: true\n models: [{name: model-a}]\n"; + let result = ensure_claude_desktop_model_aliases_in_yaml( + input, + &ClaudeDesktopModelMappings::all("model-a"), + &test_agent_models(&["model-a"]), + ); + assert!(result.is_err()); +} + +#[test] +fn disabled_provider_alias_does_not_mark_an_active_real_model_as_an_alias() { + let input = "openai-compatibility:\n - name: disabled-provider\n disabled: true\n models: [{name: other-model, alias: model-a}]\ncodex-api-key:\n - models: [{name: model-a}]\n"; + let mut models = test_agent_models(&["model-a"]); + mark_configured_agent_model_aliases(&mut models, input).unwrap(); + assert!(!models[0].is_alias); +}