From 8c80ce11511dc694e7dc01045df160b5535d9c57 Mon Sep 17 00:00:00 2001 From: lzt404 <2596933790@qq.com> Date: Wed, 26 Aug 2026 01:48:36 +0800 Subject: [PATCH] fix(model-alias): apply reasoning effort overrides by model source MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Decouple reasoning effort from the Fast service tier - Generate native override parameters for each provider - Validate efforts against the model’s supported levels - Preserve complete reasoning capabilities on configured models - Add coverage for multi-provider alias overrides --- src-tauri/src/agents/commands.rs | 18 +- src-tauri/src/core_config/aliases.rs | 287 ++++++++++++++++----- src-tauri/src/main.rs | 1 + src-tauri/src/tests/agent_configuration.rs | 2 +- src-tauri/src/tests/model_aliases.rs | 248 ++++++++++++++++-- src-tauri/src/tests/support.rs | 4 + src/i18n/ja.ts | 36 +-- src/i18n/locales/en.ts | 36 +-- src/i18n/locales/zh-CN.ts | 36 +-- src/pages/ThinkingAliasesPage.tsx | 245 +++--------------- src/styles.css | 117 --------- tests/thinkingAliases.test.ts | 111 ++------ 12 files changed, 552 insertions(+), 589 deletions(-) diff --git a/src-tauri/src/agents/commands.rs b/src-tauri/src/agents/commands.rs index a51d24e..23b2337 100644 --- a/src-tauri/src/agents/commands.rs +++ b/src-tauri/src/agents/commands.rs @@ -437,7 +437,6 @@ pub(crate) async fn create_thinking_alias( source_id: String, alias: String, effort: String, - fast: Option, ) -> Result, String> { let config = gui_config_state.snapshot()?; let source_id = source_id.trim().to_string(); @@ -450,15 +449,12 @@ pub(crate) async fn create_thinking_alias( } else { validate_thinking_alias_effort(&effort)? }; - let fast = fast.unwrap_or(false); let content = fetch_management_config_yaml(&config).await?; let available_models = fetch_agent_models(config.port, effective_agent_api_key(&config)).await?; let definitions = fetch_oauth_model_definitions(&config).await; let capability = if !effort.is_empty() { AliasSourceCapability::Reasoning - } else if fast { - AliasSourceCapability::Fast } else { AliasSourceCapability::Base }; @@ -471,6 +467,18 @@ pub(crate) async fn create_thinking_alias( .ok_or_else(|| { "原模型已不在内核当前可用模型中,或其配置来源已经变化,请刷新后重新选择".to_string() })?; + if !effort.is_empty() + && !source + .source + .reasoning_levels + .iter() + .any(|level| level.eq_ignore_ascii_case(&effort)) + { + return Err(format!( + "思考强度 {effort} 不在模型 {} 当前支持的等级中", + source.source.model + )); + } if source.source.model.eq_ignore_ascii_case(&alias) { return Err("别名模型不能和原模型相同".to_string()); } @@ -490,7 +498,7 @@ pub(crate) async fn create_thinking_alias( return Err(format!("别名模型 {alias} 已存在")); } - let updated = add_model_alias_to_yaml(&content, &source, &alias, &effort, fast)?; + let updated = add_model_alias_to_yaml(&content, &source, &alias, &effort)?; put_management_alias_config_changes(&config, &content, &updated).await?; thinking_aliases_from_yaml(&updated) } diff --git a/src-tauri/src/core_config/aliases.rs b/src-tauri/src/core_config/aliases.rs index a2e24ea..6ab227c 100644 --- a/src-tauri/src/core_config/aliases.rs +++ b/src-tauri/src/core_config/aliases.rs @@ -17,7 +17,7 @@ pub(crate) const OAUTH_ALIAS_CHANNELS: [OAuthAliasChannel; 7] = [ provider: "Vertex OAuth", kind: "vertex-oauth", protocol: "gemini", - supports_reasoning: false, + supports_reasoning: true, supports_fast: false, force_mapping: false, }, @@ -26,7 +26,7 @@ pub(crate) const OAUTH_ALIAS_CHANNELS: [OAuthAliasChannel; 7] = [ provider: "AI Studio OAuth", kind: "aistudio-oauth", protocol: "gemini", - supports_reasoning: false, + supports_reasoning: true, supports_fast: false, force_mapping: false, }, @@ -35,7 +35,7 @@ pub(crate) const OAUTH_ALIAS_CHANNELS: [OAuthAliasChannel; 7] = [ provider: "Antigravity OAuth", kind: "antigravity-oauth", protocol: "antigravity", - supports_reasoning: false, + supports_reasoning: true, supports_fast: false, force_mapping: true, }, @@ -44,7 +44,7 @@ pub(crate) const OAUTH_ALIAS_CHANNELS: [OAuthAliasChannel; 7] = [ provider: "Claude OAuth", kind: "claude-oauth", protocol: "claude", - supports_reasoning: false, + supports_reasoning: true, supports_fast: false, force_mapping: false, }, @@ -62,7 +62,7 @@ pub(crate) const OAUTH_ALIAS_CHANNELS: [OAuthAliasChannel; 7] = [ provider: "Kimi OAuth", kind: "kimi-oauth", protocol: "openai", - supports_reasoning: false, + supports_reasoning: true, supports_fast: false, force_mapping: false, }, @@ -70,8 +70,8 @@ pub(crate) const OAUTH_ALIAS_CHANNELS: [OAuthAliasChannel; 7] = [ key: "xai", provider: "xAI OAuth", kind: "xai-oauth", - protocol: "openai", - supports_reasoning: false, + protocol: "codex", + supports_reasoning: true, supports_fast: false, force_mapping: false, }, @@ -873,8 +873,32 @@ pub(crate) fn resolved_oauth_alias_sources( available_models, &mut sources, )?; - if capability == AliasSourceCapability::Fast { - sources.retain(|source| model_supports_fast(&source.source.model)); + collect_config_thinking_alias_sources( + root, + "claude-api-key", + "Claude API", + "claude-api", + "claude", + available_models, + &mut sources, + )?; + collect_config_thinking_alias_sources( + root, + "gemini-api-key", + "Gemini API", + "gemini-api", + "gemini", + available_models, + &mut sources, + )?; + match capability { + AliasSourceCapability::Reasoning => { + sources.retain(|source| !source.source.reasoning_levels.is_empty()) + } + AliasSourceCapability::Fast => { + sources.retain(|source| model_supports_fast(&source.source.model)) + } + AliasSourceCapability::Base => {} } let configured_codex_api_models = sources .iter() @@ -913,6 +937,7 @@ pub(crate) fn resolved_oauth_alias_sources( provider: channel.provider.to_string(), kind: channel.kind.to_string(), protocol: channel.protocol.to_string(), + reasoning_levels: definition.reasoning_levels.clone(), }, location: ThinkingAliasSourceLocation::Oauth { channel: channel.key, @@ -995,6 +1020,7 @@ pub(crate) fn collect_config_thinking_alias_sources( { continue; } + let reasoning_levels = configured_model_reasoning_levels(model, protocol); sources.push(ResolvedThinkingAliasSource { source: ThinkingAliasSource { id: format!("{section}:{provider_index}:{model_index}"), @@ -1003,6 +1029,7 @@ pub(crate) fn collect_config_thinking_alias_sources( provider: provider_name.clone(), kind: kind.to_string(), protocol: protocol.to_string(), + reasoning_levels, }, location: ThinkingAliasSourceLocation::ConfigModel { section, @@ -1015,6 +1042,37 @@ pub(crate) fn collect_config_thinking_alias_sources( Ok(()) } +pub(crate) fn configured_model_reasoning_levels( + model: &serde_norway::Value, + protocol: &str, +) -> Vec { + let mut levels = model + .as_mapping() + .and_then(|model| yaml_mapping_value(model, "thinking")) + .and_then(serde_norway::Value::as_mapping) + .and_then(|thinking| yaml_mapping_value(thinking, "levels")) + .and_then(serde_norway::Value::as_sequence) + .into_iter() + .flatten() + .filter_map(serde_norway::Value::as_str) + .map(str::trim) + .filter(|level| !level.is_empty()) + .map(str::to_ascii_lowercase) + .fold(Vec::new(), |mut result, level| { + if !result.contains(&level) { + result.push(level); + } + result + }); + if levels.is_empty() && matches!(protocol, "codex" | "openai") { + levels = ["low", "medium", "high", "xhigh", "max"] + .into_iter() + .map(str::to_string) + .collect(); + } + levels +} + pub(crate) fn thinking_alias_provider_name( provider: &serde_norway::Mapping, fallback: &str, @@ -1129,6 +1187,22 @@ pub(crate) fn thinking_aliases_from_value( "openai", &mut entries, )?; + collect_config_thinking_alias_entries( + root, + "claude-api-key", + "Claude API", + "claude-api", + "claude", + &mut entries, + )?; + collect_config_thinking_alias_entries( + root, + "gemini-api-key", + "Gemini API", + "gemini-api", + "gemini", + &mut entries, + )?; entries.sort_by(|left, right| { left.provider .to_ascii_lowercase() @@ -1339,15 +1413,34 @@ pub(crate) fn find_thinking_alias_effort( let effort = yaml_mapping_value(rule, "params") .and_then(serde_norway::Value::as_mapping) .and_then(|params| { - if protocol.eq_ignore_ascii_case("openai") { - yaml_mapping_value(params, "reasoning_effort") - .or_else(|| yaml_mapping_value(params, "reasoning.effort")) - } else { - yaml_mapping_value(params, "reasoning.effort") - } + let explicit = [ + "reasoning.effort", + "reasoning_effort", + "output_config.effort", + "generationConfig.thinkingConfig.thinkingLevel", + "thinking.effort", + ] + .into_iter() + .find_map(|key| yaml_mapping_value(params, key)) + .and_then(serde_norway::Value::as_str) + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(str::to_ascii_lowercase); + explicit.or_else(|| { + let thinking_type = yaml_mapping_value(params, "thinking.type") + .and_then(serde_norway::Value::as_str) + .map(str::trim)?; + if thinking_type.eq_ignore_ascii_case("disabled") { + Some("none".to_string()) + } else if protocol.eq_ignore_ascii_case("claude") + && thinking_type.eq_ignore_ascii_case("adaptive") + { + Some("auto".to_string()) + } else { + None + } + }) }) - .and_then(serde_norway::Value::as_str) - .map(str::trim) .filter(|value| !value.is_empty()); let Some(effort) = effort else { continue; @@ -1361,7 +1454,7 @@ pub(crate) fn find_thinking_alias_effort( .iter() .any(|model| thinking_payload_model_matches(model, alias, protocol)) { - return Some(effort.to_ascii_lowercase()); + return Some(effort); } } None @@ -1418,18 +1511,103 @@ pub(crate) fn thinking_payload_model_matches( name_matches && protocol_matches } +pub(crate) fn thinking_payload_model_name_matches( + model: &serde_norway::Value, + alias: &str, +) -> bool { + model + .as_mapping() + .and_then(|model| yaml_mapping_value(model, "name")) + .and_then(serde_norway::Value::as_str) + .is_some_and(|name| name.trim().eq_ignore_ascii_case(alias)) +} + +pub(crate) fn insert_thinking_effort_params( + params: &mut serde_norway::Mapping, + source: &ThinkingAliasSource, + effort: &str, +) -> Result<(), String> { + let insert = |params: &mut serde_norway::Mapping, key: &str, value: &str| { + params.insert( + yaml_key(key), + serde_norway::Value::String(value.to_string()), + ); + }; + match source.kind.as_str() { + "claude-oauth" | "claude-api" => { + if effort.eq_ignore_ascii_case("none") { + insert(params, "thinking.type", "disabled"); + } else { + insert(params, "thinking.type", "adaptive"); + if !effort.eq_ignore_ascii_case("auto") { + insert(params, "output_config.effort", effort); + } + } + } + "aistudio-oauth" | "vertex-oauth" | "gemini-api" => { + insert( + params, + "generationConfig.thinkingConfig.thinkingLevel", + effort, + ); + } + "antigravity-oauth" => { + // Antigravity applies payload rules relative to its `request` object. + insert( + params, + "generationConfig.thinkingConfig.thinkingLevel", + effort, + ); + } + "kimi-oauth" => { + if effort.eq_ignore_ascii_case("none") { + insert(params, "thinking.type", "disabled"); + } else { + insert(params, "thinking.type", "enabled"); + insert(params, "thinking.effort", effort); + } + } + "codex-oauth" | "codex-api" | "xai-oauth" => { + insert(params, "reasoning.effort", effort); + } + "openai-compatible" => { + insert(params, "reasoning_effort", effort); + if source.model.to_ascii_lowercase().starts_with("deepseek") { + insert(params, "thinking.type", "enabled"); + } + } + _ => match source.protocol.as_str() { + "codex" => insert(params, "reasoning.effort", effort), + "openai" => insert(params, "reasoning_effort", effort), + "claude" => { + if effort.eq_ignore_ascii_case("none") { + insert(params, "thinking.type", "disabled"); + } else { + insert(params, "thinking.type", "adaptive"); + if !effort.eq_ignore_ascii_case("auto") { + insert(params, "output_config.effort", effort); + } + } + } + "gemini" | "antigravity" => insert( + params, + "generationConfig.thinkingConfig.thinkingLevel", + effort, + ), + protocol => { + return Err(format!("暂不支持为 {protocol} 来源强制覆写思考强度")); + } + }, + } + Ok(()) +} + pub(crate) fn add_model_alias_to_yaml( content: &str, source: &ResolvedThinkingAliasSource, alias: &str, effort: &str, - fast: bool, ) -> Result { - if fast && !alias_source_supports_fast(source) { - return Err( - "Fast 仅支持 OpenAI 兼容 API、Codex API 或 Codex OAuth 的 GPT 系列模型".to_string(), - ); - } let mut document = yaml_serde_edit::YamlValue::parse(content) .map_err(|error| format!("解析内核 YAML 配置失败: {error}"))?; let mut updated = document.get().clone(); @@ -1462,7 +1640,7 @@ pub(crate) fn add_model_alias_to_yaml( } remove_thinking_payload_model(root, alias)?; - if effort.is_empty() && !fast { + if effort.is_empty() { return render_updated_core_yaml(&mut document, updated); } let payload = root @@ -1487,33 +1665,7 @@ pub(crate) fn add_model_alias_to_yaml( ); let mut params_mapping = serde_norway::Mapping::new(); if !effort.is_empty() { - params_mapping.insert( - yaml_key(if source.source.protocol == "openai" { - "reasoning_effort" - } else { - "reasoning.effort" - }), - serde_norway::Value::String(effort.to_string()), - ); - } - if fast { - params_mapping.insert( - yaml_key("service_tier"), - serde_norway::Value::String("priority".to_string()), - ); - } - if !effort.is_empty() - && source.source.protocol == "openai" - && source - .source - .model - .to_ascii_lowercase() - .starts_with("deepseek") - { - params_mapping.insert( - yaml_key("thinking.type"), - serde_norway::Value::String("enabled".to_string()), - ); + insert_thinking_effort_params(&mut params_mapping, &source.source, effort)?; } let mut rule_mapping = serde_norway::Mapping::new(); rule_mapping.insert( @@ -1695,15 +1847,6 @@ pub(crate) fn append_config_thinking_alias( serde_norway::Value::String(format!("{display_name} ({effort})")), ); } - let thinking = alias_model - .entry(yaml_key("thinking")) - .or_insert_with(|| serde_norway::Value::Mapping(serde_norway::Mapping::new())) - .as_mapping_mut() - .ok_or_else(|| "模型 thinking 必须是映射".to_string())?; - thinking.insert( - yaml_key("levels"), - serde_norway::Value::Sequence(vec![serde_norway::Value::String(effort.to_string())]), - ); } models.push(serde_norway::Value::Mapping(alias_model)); Ok(()) @@ -1786,6 +1929,8 @@ pub(crate) fn remove_thinking_alias_from_yaml_for_channel( if oauth_channel.is_none() { removed |= remove_config_thinking_alias(root, "codex-api-key", "codex", alias)?; removed |= remove_config_thinking_alias(root, "openai-compatibility", "openai", alias)?; + removed |= remove_config_thinking_alias(root, "claude-api-key", "claude", alias)?; + removed |= remove_config_thinking_alias(root, "gemini-api-key", "gemini", alias)?; } if !removed { return Err(format!("别名模型 {alias} 不存在,请刷新后重试")); @@ -1884,7 +2029,7 @@ pub(crate) fn configured_model_alias_exists(root: &serde_norway::Mapping, alias: }) }); oauth_exists - || ["codex-api-key", "openai-compatibility"] + || MODEL_ALIAS_CONFIG_SECTIONS .into_iter() .filter_map(|section| yaml_mapping_value(root, section)) .filter_map(serde_norway::Value::as_sequence) @@ -1996,8 +2141,16 @@ pub(crate) fn remove_thinking_payload_model( let has_effort = yaml_mapping_value(rule_mapping, "params") .and_then(serde_norway::Value::as_mapping) .is_some_and(|params| { - yaml_mapping_value(params, "reasoning.effort").is_some() - || yaml_mapping_value(params, "reasoning_effort").is_some() + [ + "reasoning.effort", + "reasoning_effort", + "output_config.effort", + "generationConfig.thinkingConfig.thinkingLevel", + "thinking.effort", + ] + .into_iter() + .any(|key| yaml_mapping_value(params, key).is_some()) + || yaml_mapping_value(params, "thinking.type").is_some() }); if has_effort { if let Some(models) = yaml_mapping_value_mut(rule_mapping, "models") { @@ -2005,10 +2158,8 @@ pub(crate) fn remove_thinking_payload_model( .as_sequence_mut() .ok_or_else(|| "payload.override.models 必须是数组".to_string())?; let before = models.len(); - models.retain(|model| { - !thinking_payload_model_matches(model, alias, "codex") - && !thinking_payload_model_matches(model, alias, "openai") - }); + models + .retain(|model| !thinking_payload_model_name_matches(model, alias)); removed_from_rule = models.len() != before; models_empty = models.is_empty(); } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 23bb2bb..9b1aa16 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1118,6 +1118,7 @@ struct ThinkingAliasSource { provider: String, kind: String, protocol: String, + reasoning_levels: Vec, } #[derive(Clone, Debug, PartialEq, Eq)] diff --git a/src-tauri/src/tests/agent_configuration.rs b/src-tauri/src/tests/agent_configuration.rs index 54764a0..844e810 100644 --- a/src-tauri/src/tests/agent_configuration.rs +++ b/src-tauri/src/tests/agent_configuration.rs @@ -2055,7 +2055,7 @@ fn thinking_alias_prefers_codex_api_key_model_over_same_named_oauth_definition() )); let rendered = - add_model_alias_to_yaml(input, &sources[0], "gpt-5.6-luna-xhigh", "xhigh", false).unwrap(); + add_model_alias_to_yaml(input, &sources[0], "gpt-5.6-luna-xhigh", "xhigh").unwrap(); assert!(rendered.contains("alias: gpt-5.6-luna-xhigh"), "{rendered}"); assert!(!rendered.contains("oauth-model-alias"), "{rendered}"); } diff --git a/src-tauri/src/tests/model_aliases.rs b/src-tauri/src/tests/model_aliases.rs index 6348b6c..8a80e75 100644 --- a/src-tauri/src/tests/model_aliases.rs +++ b/src-tauri/src/tests/model_aliases.rs @@ -18,6 +18,25 @@ fn test_oauth_definition_set(channel: &str, models: &[&str]) -> OAuthModelDefini } } +fn test_oauth_thinking_source(channel: &str, model: &str) -> ResolvedThinkingAliasSource { + let channel = oauth_alias_channel(channel).unwrap(); + ResolvedThinkingAliasSource { + source: ThinkingAliasSource { + id: format!("{}:{model}", channel.kind), + model: model.to_string(), + display_name: None, + provider: channel.provider.to_string(), + kind: channel.kind.to_string(), + protocol: channel.protocol.to_string(), + reasoning_levels: vec!["low".to_string(), "high".to_string()], + }, + location: ThinkingAliasSourceLocation::Oauth { + channel: channel.key, + force_mapping: channel.force_mapping, + }, + } +} + #[test] fn oauth_auth_provider_names_map_to_alias_channels() { assert_eq!( @@ -32,6 +51,197 @@ fn oauth_auth_provider_names_map_to_alias_channels() { assert_eq!(normalize_oauth_alias_channel("grok"), Some("xai")); } +#[test] +fn oauth_reasoning_sources_follow_each_model_definition() { + let definition_sets = [ + "vertex", + "aistudio", + "antigravity", + "claude", + "codex", + "kimi", + "xai", + ] + .into_iter() + .map(|channel| { + let mut definitions = test_oauth_definition_set(channel, &[&format!("{channel}-model")]); + definitions.models[0].reasoning_levels = vec!["low".to_string(), "high".to_string()]; + definitions + }) + .collect::>(); + let available = definition_sets + .iter() + .map(|definitions| definitions.models[0].id.as_str()) + .collect::>(); + let sources = resolved_oauth_alias_sources( + "{}\n", + &definition_sets, + &test_agent_models(&available), + AliasSourceCapability::Reasoning, + ) + .unwrap(); + + assert_eq!(sources.len(), definition_sets.len()); + assert!(sources + .iter() + .all(|source| source.source.reasoning_levels == ["low", "high"])); + assert_eq!( + sources + .iter() + .find(|source| source.source.kind == "xai-oauth") + .unwrap() + .source + .protocol, + "codex" + ); +} + +#[test] +fn thinking_alias_uses_source_native_override_parameters() { + let cases = [ + ( + "codex", + "gpt-5.6-sol", + "high", + &["reasoning.effort: high"][..], + ), + ( + "claude", + "claude-opus-4-6", + "high", + &["thinking.type: adaptive", "output_config.effort: high"][..], + ), + ( + "claude", + "claude-opus-4-6", + "auto", + &["thinking.type: adaptive"][..], + ), + ( + "claude", + "claude-opus-4-6", + "none", + &["thinking.type: disabled"][..], + ), + ( + "aistudio", + "gemini-3.1-pro", + "high", + &["generationConfig.thinkingConfig.thinkingLevel: high"][..], + ), + ( + "antigravity", + "gemini-3.1-pro", + "high", + &["generationConfig.thinkingConfig.thinkingLevel: high"][..], + ), + ( + "kimi", + "kimi-k2.5", + "high", + &["thinking.type: enabled", "thinking.effort: high"][..], + ), + ( + "kimi", + "kimi-k2.5", + "none", + &["thinking.type: disabled"][..], + ), + ("xai", "grok-4", "high", &["reasoning.effort: high"][..]), + ]; + + for (channel, model, effort, expected) in cases { + let source = test_oauth_thinking_source(channel, model); + let alias = format!("{model}-{effort}"); + let rendered = add_model_alias_to_yaml("{}\n", &source, &alias, effort).unwrap(); + for parameter in expected { + assert!( + rendered.contains(parameter), + "{channel}: missing {parameter}\n{rendered}" + ); + } + assert_eq!( + thinking_aliases_from_yaml(&rendered).unwrap()[0] + .effort + .as_deref(), + Some(effort) + ); + let restored = remove_thinking_alias_from_yaml(&rendered, &alias).unwrap(); + assert!(!restored.contains(&alias), "{channel}: {restored}"); + } +} + +#[test] +fn configured_claude_and_gemini_models_use_native_overrides() { + let input = "claude-api-key:\n - api-key: claude-key\n models:\n - name: claude-opus-4-6\n thinking:\n levels: [low, high]\ngemini-api-key:\n - api-key: gemini-key\n models:\n - name: gemini-3.1-pro\n thinking:\n levels: [low, high]\n"; + let available_models = test_agent_models(&["claude-opus-4-6", "gemini-3.1-pro"]); + let sources = resolved_oauth_alias_sources( + input, + &[], + &available_models, + AliasSourceCapability::Reasoning, + ) + .unwrap(); + + let claude = sources + .iter() + .find(|source| source.source.kind == "claude-api") + .unwrap(); + let with_claude = add_model_alias_to_yaml(input, claude, "claude-fixed", "high").unwrap(); + assert!( + with_claude.contains("output_config.effort: high"), + "{with_claude}" + ); + + let sources = resolved_oauth_alias_sources( + &with_claude, + &[], + &available_models, + AliasSourceCapability::Reasoning, + ) + .unwrap(); + let gemini = sources + .iter() + .find(|source| source.source.kind == "gemini-api") + .unwrap(); + let rendered = add_model_alias_to_yaml(&with_claude, gemini, "gemini-fixed", "low").unwrap(); + assert!( + rendered.contains("generationConfig.thinkingConfig.thinkingLevel: low"), + "{rendered}" + ); + assert!(thinking_aliases_from_yaml(&rendered) + .unwrap() + .iter() + .any(|entry| entry.alias == "claude-fixed")); + assert!(thinking_aliases_from_yaml(&rendered) + .unwrap() + .iter() + .any(|entry| entry.alias == "gemini-fixed")); + let document: serde_norway::Value = serde_norway::from_str(&rendered).unwrap(); + let root = document.as_mapping().unwrap(); + for (section, alias, protocol) in [ + ("claude-api-key", "claude-fixed", "claude"), + ("gemini-api-key", "gemini-fixed", "gemini"), + ] { + let alias_model = yaml_mapping_value(root, section) + .and_then(serde_norway::Value::as_sequence) + .and_then(|providers| providers[0].as_mapping()) + .and_then(|provider| yaml_mapping_value(provider, "models")) + .and_then(serde_norway::Value::as_sequence) + .unwrap() + .iter() + .find(|model| { + configured_model_identity(model) + .is_some_and(|(_, model_alias, _)| model_alias == alias) + }) + .unwrap(); + assert_eq!( + configured_model_reasoning_levels(alias_model, protocol), + ["low", "high"] + ); + } +} + #[test] fn antigravity_alias_uses_its_own_oauth_channel_and_force_mapping() { let available_models = test_agent_models(&["gemini-pro-agent"]); @@ -50,7 +260,7 @@ fn antigravity_alias_uses_its_own_oauth_channel_and_force_mapping() { assert_eq!(sources.len(), 1); assert_eq!(sources[0].source.kind, "antigravity-oauth"); let rendered = - add_model_alias_to_yaml("{}\n", &sources[0], "gemini-3.1-pro-preview", "", false).unwrap(); + add_model_alias_to_yaml("{}\n", &sources[0], "gemini-3.1-pro-preview", "").unwrap(); assert!(rendered.contains("antigravity:"), "{rendered}"); assert!(rendered.contains("name: gemini-pro-agent"), "{rendered}"); @@ -99,6 +309,7 @@ fn fast_is_only_available_for_gpt_models_from_supported_sources() { provider: "Relay".to_string(), kind: "openai-compatible".to_string(), protocol: "openai".to_string(), + reasoning_levels: vec!["low".to_string(), "medium".to_string(), "high".to_string()], }, location: ThinkingAliasSourceLocation::ConfigModel { section: "openai-compatibility", @@ -107,9 +318,6 @@ fn fast_is_only_available_for_gpt_models_from_supported_sources() { }, }; assert!(add_speed_alias_to_yaml(input, &deepseek_source, "deepseek-fast").is_err()); - assert!( - add_model_alias_to_yaml(input, &deepseek_source, "deepseek-fast", "high", true).is_err() - ); let antigravity_source = ResolvedThinkingAliasSource { source: ThinkingAliasSource { @@ -119,6 +327,7 @@ fn fast_is_only_available_for_gpt_models_from_supported_sources() { provider: "Antigravity OAuth".to_string(), kind: "antigravity-oauth".to_string(), protocol: "antigravity".to_string(), + reasoning_levels: vec!["low".to_string(), "high".to_string()], }, location: ThinkingAliasSourceLocation::Oauth { channel: "antigravity", @@ -134,15 +343,13 @@ fn fast_is_only_available_for_gpt_models_from_supported_sources() { .unwrap(); assert!(antigravity_fast_sources.is_empty()); assert!(add_speed_alias_to_yaml("{}\n", &antigravity_source, "gpt-fast").is_err()); - assert!(add_model_alias_to_yaml("{}\n", &antigravity_source, "gpt-fast", "", true).is_err()); } #[test] fn thinking_alias_adds_fork_and_matching_payload_rule() { let input = "# Keep this comment\ndebug: true\npayload:\n override:\n - models:\n - name: existing-fast\n protocol: codex\n params:\n service_tier: priority\n"; let source = test_codex_oauth_thinking_source("gpt-5.5"); - let rendered = - add_model_alias_to_yaml(input, &source, "gpt-5.5-xhigh", "xhigh", false).unwrap(); + let rendered = add_model_alias_to_yaml(input, &source, "gpt-5.5-xhigh", "xhigh").unwrap(); let aliases = thinking_aliases_from_yaml(&rendered).unwrap(); assert!(rendered.contains("# Keep this comment"), "{rendered}"); @@ -163,7 +370,7 @@ fn thinking_alias_adds_fork_and_matching_payload_rule() { #[test] fn model_alias_can_be_created_without_overrides() { let source = test_codex_oauth_thinking_source("gpt-5.5"); - let rendered = add_model_alias_to_yaml("{}\n", &source, "gpt-5.5-alias", "", false).unwrap(); + let rendered = add_model_alias_to_yaml("{}\n", &source, "gpt-5.5-alias", "").unwrap(); assert!(rendered.contains("alias: gpt-5.5-alias"), "{rendered}"); assert!(!rendered.contains("payload:"), "{rendered}"); @@ -189,7 +396,7 @@ fn configured_model_alias_can_be_created_without_overrides() { .iter() .find(|source| source.source.model == "gpt-custom") .unwrap(); - let rendered = add_model_alias_to_yaml(input, source, "gpt-custom-alias", "", false).unwrap(); + let rendered = add_model_alias_to_yaml(input, source, "gpt-custom-alias", "").unwrap(); assert!(rendered.contains("alias: gpt-custom-alias"), "{rendered}"); assert!(!rendered.contains("thinking:"), "{rendered}"); @@ -200,20 +407,7 @@ fn configured_model_alias_can_be_created_without_overrides() { } #[test] -fn model_alias_can_apply_reasoning_and_fast_together() { - let source = test_codex_oauth_thinking_source("gpt-5.6-sol"); - let rendered = - add_model_alias_to_yaml("{}\n", &source, "gpt-5.6-sol-xhigh-fast", "xhigh", true).unwrap(); - - assert!(rendered.contains("reasoning.effort: xhigh"), "{rendered}"); - assert!(rendered.contains("service_tier: priority"), "{rendered}"); - assert_eq!(thinking_aliases_from_yaml(&rendered).unwrap().len(), 1); - assert_eq!(speed_aliases_from_yaml(&rendered).unwrap().len(), 1); - - let restored = remove_thinking_alias_from_yaml(&rendered, "gpt-5.6-sol-xhigh-fast").unwrap(); - assert!(!restored.contains("gpt-5.6-sol-xhigh-fast"), "{restored}"); - assert!(!restored.contains("service_tier: priority"), "{restored}"); - +fn thinking_alias_removal_cleans_legacy_combined_rules() { let legacy = "oauth-model-alias:\n codex:\n - name: gpt-5.6-sol\n alias: legacy-combined\n fork: true\npayload:\n override:\n - models:\n - name: legacy-combined\n protocol: codex\n params:\n reasoning.effort: high\n - models:\n - name: legacy-combined\n protocol: codex\n params:\n service_tier: priority\n"; let restored = remove_thinking_alias_from_yaml(legacy, "legacy-combined").unwrap(); assert!(!restored.contains("legacy-combined"), "{restored}"); @@ -323,7 +517,7 @@ fn thinking_alias_rejects_duplicate_client_visible_name() { let input = "oauth-model-alias:\n codex:\n - name: gpt-5.5\n alias: gpt-5.5-high\n fork: true\n"; let source = test_codex_oauth_thinking_source("gpt-5.4"); assert!( - add_model_alias_to_yaml(input, &source, "GPT-5.5-HIGH", "high", false) + add_model_alias_to_yaml(input, &source, "GPT-5.5-HIGH", "high") .unwrap_err() .contains("已存在") ); @@ -338,8 +532,7 @@ fn thinking_alias_supports_openai_compatible_model_entries() { .iter() .find(|source| source.source.model == "deepseek-chat") .unwrap(); - let rendered = - add_model_alias_to_yaml(input, source, "deepseek-chat-high", "high", false).unwrap(); + let rendered = add_model_alias_to_yaml(input, source, "deepseek-chat-high", "high").unwrap(); let value: serde_norway::Value = serde_norway::from_str(&rendered).unwrap(); let root = value.as_mapping().unwrap(); let providers = yaml_mapping_value(root, "openai-compatibility") @@ -383,8 +576,7 @@ fn thinking_alias_supports_codex_api_model_entries() { .iter() .find(|source| source.source.kind == "codex-api") .unwrap(); - let rendered = - add_model_alias_to_yaml(input, source, "gpt-custom-xhigh", "xhigh", false).unwrap(); + let rendered = add_model_alias_to_yaml(input, source, "gpt-custom-xhigh", "xhigh").unwrap(); assert!(rendered.contains("alias: gpt-custom-xhigh"), "{rendered}"); assert!(rendered.contains("protocol: codex"), "{rendered}"); diff --git a/src-tauri/src/tests/support.rs b/src-tauri/src/tests/support.rs index cb243e4..a306e71 100644 --- a/src-tauri/src/tests/support.rs +++ b/src-tauri/src/tests/support.rs @@ -42,6 +42,10 @@ pub(super) fn test_codex_oauth_thinking_source(model: &str) -> ResolvedThinkingA provider: "Codex OAuth".to_string(), kind: "codex-oauth".to_string(), protocol: "codex".to_string(), + reasoning_levels: vec!["low", "medium", "high", "xhigh", "max"] + .into_iter() + .map(str::to_string) + .collect(), }, location: ThinkingAliasSourceLocation::Oauth { channel: "codex", diff --git a/src/i18n/ja.ts b/src/i18n/ja.ts index 90f2d1e..5d4fa5b 100644 --- a/src/i18n/ja.ts +++ b/src/i18n/ja.ts @@ -237,15 +237,13 @@ export const jaOverrides = { 'aliases.error.selectModel': '先に元のモデルを選択してください', 'aliases.error.emptyAlias': 'エイリアスモデル名を入力してください', 'aliases.error.emptyEffort': '推論レベルを入力してください', - 'aliases.error.emptyOptions': '推論レベルを設定するか、Fast を有効にしてください', - 'aliases.error.unsupportedEffort': 'このモデルでは推論レベルを設定できないため、Fast オプションのみ使用できます', - 'aliases.error.unsupportedFast': 'このモデルでは Fast を利用できません', + 'aliases.error.unsupportedEffort': 'このモデルソースには利用可能な推論レベルがありません', 'aliases.created': '{alias} を作成し、推論レベルを {effort} に固定しました', - 'aliases.createdCombined': '{alias} を作成し、推論レベル {effort} と Fast を有効にしました', - 'aliases.deleteConfirm': 'エイリアスモデル「{alias}」を削除しますか?\n対応する推論レベルと Fast の上書きルールも削除されます。', + 'aliases.createdPlain': 'モデルエイリアス {alias} を作成しました', + 'aliases.deleteConfirm': 'エイリアスモデル「{alias}」を削除しますか?\n対応する推論レベルの上書きルールも削除されます。', 'aliases.deleted': '{alias} を削除しました', 'aliases.create.title': 'モデルエイリアスを作成', - 'aliases.create.description': '推論レベルと Fast 速度階層は個別にも同時にも設定できます', + 'aliases.create.description': 'モデルに別の入口を作成し、選択したソースの推論レベルを固定します', 'aliases.originalModel': '元のモデル', 'aliases.loadingModels': 'モデルを読み込んでいます', 'aliases.searchModel': '元のモデルを検索して選択', @@ -253,22 +251,12 @@ export const jaOverrides = { 'aliases.noMatch': '一致するモデルまたはソースがありません', 'aliases.noModels': '選択可能なモデルがありません', 'aliases.sourceLabel': 'ソース:{source}', - 'aliases.sourceHint': '同名モデルは Codex OAuth、Codex API、OpenAI 互換ソースごとに表示されます', + 'aliases.sourceHint': '同名モデルは実際の接続ソースごとに分けて表示されます', 'aliases.effort.title': '推論レベル', - 'aliases.effort.description': '個別に設定することも、Fast と同時に使用することもできます', + 'aliases.effort.description': '選択したモデルソースが対応するレベルのみ表示します', 'aliases.effort.none': '設定しない', - 'aliases.effort.noneHint': '推論レベルを上書きせず、ほかの有効なオプションのみ使用します', - 'aliases.effort.unsupported': 'このモデルには利用可能な推論レベルがないため、Fast のみ有効にしました。', - 'aliases.fast.title': '速度オプション', - 'aliases.fast.description': 'Fast リクエスト階層を固定します(service_tier=priority)', - 'aliases.fast.enabled': '有効:推論レベルと同時に適用されます', - 'aliases.fast.disabled': '無効', - 'aliases.customLevel': 'カスタムレベル:{effort}', - 'aliases.customLevelHint': 'モデルプロバイダーが対応する別の推論レベルを入力', - 'aliases.custom': 'カスタム', - 'aliases.customPlaceholder': '例:minimal、auto、ultra、またはベンダー独自レベル', - 'aliases.collapseCustom': 'カスタム入力を折りたたむ', - 'aliases.customHelp': '対象モデルが実際に対応するレベル名のみ入力してください', + 'aliases.effort.noneHint': '推論レベルを上書きしません', + 'aliases.effort.unsupported': 'このモデルソースは利用可能な推論レベルを報告していません。', 'aliases.aliasName.title': 'エイリアスモデル名', 'aliases.aliasName.description': '新しい入口のみを追加し、元のモデル名は変更しません', 'aliases.aliasName.autoDescription': 'モデルと選択したオプションから自動生成します。元のモデル名は変更せず編集できます', @@ -280,18 +268,12 @@ export const jaOverrides = { 'aliases.creating': '作成中', 'aliases.create': 'エイリアスを作成', 'aliases.createdList.title': '作成済みエイリアス', - 'aliases.createdList.description': '各エイリアスの推論レベルと Fast 状態をまとめて表示します', + 'aliases.createdList.description': '各エイリアスに固定された推論レベルを表示します', 'aliases.loadingConfig': '設定を読み込んでいます', 'aliases.empty.title': 'モデルエイリアスはまだありません', 'aliases.empty.description': '左側でモデルとエイリアスオプションを選択して作成できます', 'aliases.unboundEffort': 'レベル未設定', 'aliases.delete': '{alias} を削除', - 'speedAliases.created': '{alias} を作成し、速度階層を Fast に固定しました', - 'speedAliases.deleted': '{alias} を削除しました', - 'speedAliases.deleteConfirm': '速度エイリアス「{alias}」を削除しますか?\n対応する Fast 上書きルールも削除されます。', - 'speedAliases.fast.title': 'Fast', - 'speedAliases.aliasName.example': '例:{model}-fast', - 'speedAliases.delete': '{alias} を削除', 'config.routing.roundRobin': 'ラウンドロビン', 'config.routing.fillFirst': '優先充填', 'config.tabs.label': '詳細設定ページ', diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 1c0365b..377430a 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -237,15 +237,13 @@ export const en: Record = { 'aliases.error.selectModel': 'Select an original model first', 'aliases.error.emptyAlias': 'The model alias cannot be empty', 'aliases.error.emptyEffort': 'The reasoning effort cannot be empty', - 'aliases.error.emptyOptions': 'Set a reasoning effort or enable Fast', - 'aliases.error.unsupportedEffort': 'This model does not support a reasoning-effort override; only Fast can be used', - 'aliases.error.unsupportedFast': 'Fast is not available for this model', + 'aliases.error.unsupportedEffort': 'This model source has no available reasoning-effort levels', 'aliases.created': 'Created {alias} with reasoning effort fixed to {effort}', - 'aliases.createdCombined': 'Created {alias} with reasoning effort {effort} and Fast enabled', - 'aliases.deleteConfirm': 'Delete the model alias “{alias}”?\nIts reasoning-effort and Fast overrides will also be removed.', + 'aliases.createdPlain': 'Created model alias {alias}', + 'aliases.deleteConfirm': 'Delete the model alias “{alias}”?\nIts reasoning-effort override will also be removed.', 'aliases.deleted': 'Deleted {alias}', 'aliases.create.title': 'Create Model Alias', - 'aliases.create.description': 'Reasoning effort and the Fast service tier can be configured independently or together', + 'aliases.create.description': 'Create a separate model entry and fix its reasoning effort for the selected source', 'aliases.originalModel': 'Original Model', 'aliases.loadingModels': 'Loading models', 'aliases.searchModel': 'Search and select an original model', @@ -253,22 +251,12 @@ export const en: Record = { 'aliases.noMatch': 'No matching models or sources', 'aliases.noModels': 'No models are currently available', 'aliases.sourceLabel': 'Source: {source}', - 'aliases.sourceHint': 'Models with the same name are listed separately for Codex OAuth, Codex API, and OpenAI-compatible sources', + 'aliases.sourceHint': 'Models with the same name are listed separately for each configured source', 'aliases.effort.title': 'Reasoning Effort', - 'aliases.effort.description': 'Configure it independently or use it together with Fast', + 'aliases.effort.description': 'Only levels reported by the selected model source are shown', 'aliases.effort.none': 'Not set', - 'aliases.effort.noneHint': 'Do not override reasoning effort; use another enabled option only', - 'aliases.effort.unsupported': 'This model has no available reasoning efforts, so only Fast has been enabled.', - 'aliases.fast.title': 'Speed Option', - 'aliases.fast.description': 'Always use the Fast request tier (service_tier=priority)', - 'aliases.fast.enabled': 'Enabled; works together with reasoning effort', - 'aliases.fast.disabled': 'Disabled', - 'aliases.customLevel': 'Custom level: {effort}', - 'aliases.customLevelHint': 'Enter another reasoning level supported by the model provider', - 'aliases.custom': 'Custom', - 'aliases.customPlaceholder': 'For example: minimal, auto, ultra, or a provider-specific level', - 'aliases.collapseCustom': 'Collapse custom input', - 'aliases.customHelp': 'Enter only a level name supported by the target model', + 'aliases.effort.noneHint': 'Do not override reasoning effort', + 'aliases.effort.unsupported': 'This model source does not report any available reasoning-effort levels.', 'aliases.aliasName.title': 'Model Alias', 'aliases.aliasName.description': 'Adds another entry point without renaming the original model', 'aliases.aliasName.autoDescription': 'Generated from the model and selected options; edit it as needed without renaming the original', @@ -280,18 +268,12 @@ export const en: Record = { 'aliases.creating': 'Creating', 'aliases.create': 'Create Alias', 'aliases.createdList.title': 'Created Aliases', - 'aliases.createdList.description': 'Each alias shows its reasoning effort and Fast status together', + 'aliases.createdList.description': 'Each alias shows its fixed reasoning effort', 'aliases.loadingConfig': 'Loading configuration', 'aliases.empty.title': 'No model aliases yet', 'aliases.empty.description': 'Select a model and alias option on the left to create one', 'aliases.unboundEffort': 'No effort bound', 'aliases.delete': 'Delete {alias}', - 'speedAliases.created': 'Created {alias} with the Fast service tier fixed', - 'speedAliases.deleted': 'Deleted {alias}', - 'speedAliases.deleteConfirm': 'Delete the speed alias “{alias}”?\nIts Fast service-tier override will also be removed.', - 'speedAliases.fast.title': 'Fast', - 'speedAliases.aliasName.example': 'For example: {model}-fast', - 'speedAliases.delete': 'Delete {alias}', 'config.routing.roundRobin': 'Round Robin', 'config.routing.fillFirst': 'Fill First', 'config.tabs.label': 'Advanced settings sections', diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts index 07f751f..bdcf29b 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -235,15 +235,13 @@ export const zhCN = { 'aliases.error.selectModel': '请先选择原模型', 'aliases.error.emptyAlias': '别名模型不能为空', 'aliases.error.emptyEffort': '思考强度不能为空', - 'aliases.error.emptyOptions': '请至少设置思考强度或启用 Fast', - 'aliases.error.unsupportedEffort': '该模型不支持设置思考强度,只能使用 Fast 选项', - 'aliases.error.unsupportedFast': '该模型当前不支持 Fast 选项', + 'aliases.error.unsupportedEffort': '该模型来源没有可用的思考强度等级', 'aliases.created': '已创建 {alias},思考强度固定为 {effort}', - 'aliases.createdCombined': '已创建 {alias},思考强度为 {effort},同时启用 Fast', - 'aliases.deleteConfirm': '确定删除别名模型“{alias}”吗?\n将同时移除对应的思考强度和 Fast 覆盖规则。', + 'aliases.createdPlain': '已创建别名 {alias}', + 'aliases.deleteConfirm': '确定删除别名模型“{alias}”吗?\n对应的思考强度覆写规则也会被移除。', 'aliases.deleted': '已删除 {alias}', 'aliases.create.title': '创建模型别名', - 'aliases.create.description': '思考强度和 Fast 速度层级可独立设置,也可以同时启用', + 'aliases.create.description': '为模型创建独立入口,并按模型来源固定思考强度', 'aliases.originalModel': '原模型', 'aliases.loadingModels': '正在读取模型', 'aliases.searchModel': '搜索并选择原模型', @@ -251,22 +249,12 @@ export const zhCN = { 'aliases.noMatch': '没有匹配的模型或来源', 'aliases.noModels': '当前没有可选模型', 'aliases.sourceLabel': '来源:{source}', - 'aliases.sourceHint': '同名模型会按 Codex OAuth、Codex API 和 OpenAI 兼容来源分别列出', + 'aliases.sourceHint': '同名模型会按实际接入来源分别列出', 'aliases.effort.title': '思考强度', - 'aliases.effort.description': '可单独设置,也可以与 Fast 同时启用', + 'aliases.effort.description': '仅显示所选模型来源实际支持的等级', 'aliases.effort.none': '不设置', - 'aliases.effort.noneHint': '不覆盖思考强度,仅使用其他已启用的选项', - 'aliases.effort.unsupported': '这个模型没有可用的思考强度,已切换为仅启用 Fast。', - 'aliases.fast.title': '速度选项', - 'aliases.fast.description': '固定使用 Fast 请求层级(service_tier=priority)', - 'aliases.fast.enabled': '已启用', - 'aliases.fast.disabled': '未启用', - 'aliases.customLevel': '自定义等级:{effort}', - 'aliases.customLevelHint': '输入模型提供商支持的其他思考等级', - 'aliases.custom': '自定义', - 'aliases.customPlaceholder': '例如 minimal、auto、ultra 或厂商等级', - 'aliases.collapseCustom': '收起自定义输入', - 'aliases.customHelp': '仅填写目标模型实际支持的等级名称', + 'aliases.effort.noneHint': '不覆盖思考强度', + 'aliases.effort.unsupported': '这个模型来源没有报告可用的思考强度等级。', 'aliases.aliasName.title': '别名模型名称', 'aliases.aliasName.description': '由你填写,仅新增入口,不会改名原模型', 'aliases.aliasName.autoDescription': '根据原模型和已选选项自动生成,可按需修改;不会改名原模型', @@ -278,18 +266,12 @@ export const zhCN = { 'aliases.creating': '创建中', 'aliases.create': '创建别名', 'aliases.createdList.title': '已创建别名', - 'aliases.createdList.description': '每个别名会同时显示已设置的思考强度和 Fast 状态', + 'aliases.createdList.description': '每个别名会显示已固定的思考强度', 'aliases.loadingConfig': '读取配置中', 'aliases.empty.title': '尚未创建模型别名', 'aliases.empty.description': '从左侧选择模型和别名选项即可创建', 'aliases.unboundEffort': '未绑定强度', 'aliases.delete': '删除 {alias}', - 'speedAliases.created': '已创建 {alias},速度层级固定为 Fast', - 'speedAliases.deleted': '已删除 {alias}', - 'speedAliases.deleteConfirm': '确定删除速度别名模型“{alias}”吗?\n将同时移除对应的 Fast 覆盖规则。', - 'speedAliases.fast.title': 'Fast', - 'speedAliases.aliasName.example': '例如 {model}-fast', - 'speedAliases.delete': '删除 {alias}', 'config.routing.roundRobin': '轮询', 'config.routing.fillFirst': '优先填充', 'config.tabs.label': '高级设置子页面', diff --git a/src/pages/ThinkingAliasesPage.tsx b/src/pages/ThinkingAliasesPage.tsx index 5876b4d..02c6c30 100644 --- a/src/pages/ThinkingAliasesPage.tsx +++ b/src/pages/ThinkingAliasesPage.tsx @@ -15,8 +15,6 @@ import { LoaderCircle, Search, Trash2, - X, - Zap, } from 'lucide-react'; import { getCurrentLocale, translate, useI18n } from '../i18n'; @@ -31,19 +29,6 @@ type ThinkingAliasEntry = { oauthChannel?: string | null; }; -type SpeedAliasEntry = { - sourceModel: string; - alias: string; - serviceTier: string; - provider: string; - kind: string; - oauthChannel?: string | null; -}; - -type AliasListEntry = ThinkingAliasEntry & { - serviceTier: string | null; -}; - type ThinkingAliasSource = { id: string; model: string; @@ -51,11 +36,11 @@ type ThinkingAliasSource = { provider: string; kind: string; protocol: string; + reasoningLevels: string[]; }; type ModelAliasSource = ThinkingAliasSource & { supportsReasoning: boolean; - supportsFast: boolean; }; const effortOptions = [ @@ -66,47 +51,16 @@ const effortOptions = [ { value: 'max', label: 'Max', hintKey: 'aliases.effort.max' }, ] as const satisfies ReadonlyArray<{ value: PresetThinkingEffort; label: string; hintKey: string }>; -export const combineModelAliasEntries = ( - thinkingEntries: ThinkingAliasEntry[], - speedEntries: SpeedAliasEntry[], -): AliasListEntry[] => { - const entries = new Map(); - const entryKey = (entry: Pick) => ( - [entry.oauthChannel ?? '', entry.kind, entry.provider, entry.sourceModel, entry.alias] - .map((value) => value.toLocaleLowerCase()) - .join('\u0000') - ); - - thinkingEntries.forEach((entry) => { - entries.set(entryKey(entry), { ...entry, serviceTier: null }); - }); - speedEntries.forEach((entry) => { - const key = entryKey(entry); - const current = entries.get(key); - entries.set(key, current - ? { ...current, serviceTier: entry.serviceTier } - : { ...entry, effort: null, serviceTier: entry.serviceTier }); - }); - - return [...entries.values()].sort((left, right) => ( - left.provider.localeCompare(right.provider) - || left.alias.localeCompare(right.alias) - )); -}; - export const combineModelAliasSources = ( baseSources: ThinkingAliasSource[], thinkingSources: ThinkingAliasSource[], - speedSources: ThinkingAliasSource[], ): ModelAliasSource[] => { const reasoningSourceIds = new Set(thinkingSources.map((source) => source.id)); - const speedSourceIds = new Set(speedSources.map((source) => source.id)); const sources = new Map(); - [...baseSources, ...speedSources, ...thinkingSources].forEach((source) => { + [...baseSources, ...thinkingSources].forEach((source) => { sources.set(source.id, { ...source, supportsReasoning: reasoningSourceIds.has(source.id), - supportsFast: speedSourceIds.has(source.id), }); }); return [...sources.values()]; @@ -115,13 +69,12 @@ export const combineModelAliasSources = ( export const defaultModelAlias = ( model: string | null | undefined, effort: string, - fast: boolean, ) => { const normalizedModel = model?.trim() ?? ''; const normalizedEffort = effort.trim().toLowerCase(); if (!normalizedModel) return ''; - if (!normalizedEffort && !fast) return `${normalizedModel}-alias`; - return `${normalizedModel}${normalizedEffort ? `-${normalizedEffort}` : ''}${fast ? '-fast' : ''}`; + if (!normalizedEffort) return `${normalizedModel}-alias`; + return `${normalizedModel}-${normalizedEffort}`; }; export const uniqueModelAlias = ( @@ -155,6 +108,8 @@ export const thinkingAliasSourceKindLabel = (kind: string) => { if (kind === 'kimi-oauth') return 'Kimi OAuth'; if (kind === 'xai-oauth') return 'xAI OAuth'; if (kind === 'codex-api') return 'Codex API'; + if (kind === 'claude-api') return 'Claude API'; + if (kind === 'gemini-api') return 'Gemini API'; if (kind === 'openai-compatible') return translate(getCurrentLocale(), 'aliases.source.openAiCompatible'); return translate(getCurrentLocale(), 'aliases.source.other'); }; @@ -172,14 +127,10 @@ const thinkingAliasSourceDetail = (source: ThinkingAliasSource) => ( export function ThinkingAliasesPage() { const { t } = useI18n(); const [thinkingEntries, setThinkingEntries] = useState([]); - const [speedEntries, setSpeedEntries] = useState([]); const [baseSources, setBaseSources] = useState([]); const [thinkingSources, setThinkingSources] = useState([]); - const [speedSources, setSpeedSources] = useState([]); const [selectedSourceId, setSelectedSourceId] = useState(''); const [effort, setEffort] = useState(''); - const [fastEnabled, setFastEnabled] = useState(false); - const [customEffortOpen, setCustomEffortOpen] = useState(false); const [alias, setAlias] = useState(''); const [search, setSearch] = useState(''); const [modelPickerOpen, setModelPickerOpen] = useState(false); @@ -196,18 +147,14 @@ export function ThinkingAliasesPage() { setLoading(true); setError(''); try { - const [nextThinkingEntries, nextBaseSources, nextThinkingSources, nextSpeedEntries, nextSpeedSources] = await Promise.all([ + const [nextThinkingEntries, nextBaseSources, nextThinkingSources] = await Promise.all([ invoke('get_thinking_aliases'), invoke('get_model_alias_sources'), invoke('get_thinking_alias_sources'), - invoke('get_speed_aliases'), - invoke('get_speed_alias_sources'), ]); setThinkingEntries(nextThinkingEntries); setBaseSources(nextBaseSources); setThinkingSources(nextThinkingSources); - setSpeedEntries(nextSpeedEntries); - setSpeedSources(nextSpeedSources); setSelectedSourceId((current) => ( nextBaseSources.some((source) => source.id === current) ? current @@ -237,12 +184,8 @@ export function ThinkingAliasesPage() { }, [modelPickerOpen]); const sources = useMemo( - () => combineModelAliasSources(baseSources, thinkingSources, speedSources), - [baseSources, speedSources, thinkingSources], - ); - const entries = useMemo( - () => combineModelAliasEntries(thinkingEntries, speedEntries), - [speedEntries, thinkingEntries], + () => combineModelAliasSources(baseSources, thinkingSources), + [baseSources, thinkingSources], ); useEffect(() => { @@ -284,14 +227,7 @@ export function ThinkingAliasesPage() { const chooseSource = (source: ModelAliasSource) => { setSelectedSourceId(source.id); - if (!source.supportsReasoning) { - setEffort(''); - setFastEnabled(source.supportsFast && (fastEnabled || !source.supportsReasoning)); - } else { - setEffort(''); - setFastEnabled(source.supportsFast && fastEnabled); - } - setCustomEffortOpen(false); + setEffort(''); setAlias(''); generatedAliasRef.current = ''; setModelPickerOpen(false); @@ -329,12 +265,15 @@ export function ThinkingAliasesPage() { }; const normalizedEffort = effort.trim().toLowerCase(); - const defaultAlias = defaultModelAlias(selectedSource?.model, normalizedEffort, fastEnabled); + const defaultAlias = defaultModelAlias(selectedSource?.model, normalizedEffort); const uniqueDefaultAlias = uniqueModelAlias(defaultAlias, sources.map((source) => source.model)); - const customEffortSelected = Boolean( - normalizedEffort - && !effortOptions.some((option) => option.value === normalizedEffort), - ); + const availableEfforts = selectedSource?.reasoningLevels ?? []; + const visibleEffortOptions = availableEfforts.map((value) => { + const preset = effortOptions.find((option) => option.value === value); + return preset + ? { value: preset.value, label: preset.label, title: t(preset.hintKey) } + : { value, label: value, title: value }; + }); useEffect(() => { setAlias((current) => { @@ -356,10 +295,6 @@ export function ThinkingAliasesPage() { setError(t('aliases.error.emptyAlias')); return; } - if (fastEnabled && !selectedSource.supportsFast) { - setError(t('aliases.error.unsupportedFast')); - return; - } if (normalizedEffort && !selectedSource.supportsReasoning) { setError(t('aliases.error.unsupportedEffort')); return; @@ -369,35 +304,14 @@ export function ThinkingAliasesPage() { setError(''); setNotice(''); try { - if (normalizedEffort) { - await invoke('create_thinking_alias', { - sourceId: selectedSource.id, - alias: normalizedAlias, - effort: normalizedEffort, - fast: fastEnabled, - }); - setNotice(t(fastEnabled ? 'aliases.createdCombined' : 'aliases.created', { - alias: normalizedAlias, - effort: normalizedEffort, - })); - } else if (fastEnabled) { - await invoke('create_speed_alias', { - sourceId: selectedSource.id, - alias: normalizedAlias, - }); - setNotice(t('speedAliases.created', { alias: normalizedAlias })); - } else { - await invoke('create_thinking_alias', { - sourceId: selectedSource.id, - alias: normalizedAlias, - effort: '', - fast: false, - }); - setNotice(t('aliases.created', { - alias: normalizedAlias, - effort: t('aliases.effort.none'), - })); - } + await invoke('create_thinking_alias', { + sourceId: selectedSource.id, + alias: normalizedAlias, + effort: normalizedEffort, + }); + setNotice(normalizedEffort + ? t('aliases.created', { alias: normalizedAlias, effort: normalizedEffort }) + : t('aliases.createdPlain', { alias: normalizedAlias })); setAlias(''); await load(); } catch (requestError) { @@ -408,7 +322,7 @@ export function ThinkingAliasesPage() { } }; - const deleteAlias = async (entry: AliasListEntry) => { + const deleteAlias = async (entry: ThinkingAliasEntry) => { if (!window.confirm( t('aliases.deleteConfirm', { alias: entry.alias }), )) return; @@ -417,17 +331,10 @@ export function ThinkingAliasesPage() { setError(''); setNotice(''); try { - if (entry.effort || !entry.serviceTier) { - await invoke('delete_thinking_alias', { - alias: entry.alias, - oauthChannel: entry.oauthChannel, - }); - } else { - await invoke('delete_speed_alias', { - alias: entry.alias, - oauthChannel: entry.oauthChannel, - }); - } + await invoke('delete_thinking_alias', { + alias: entry.alias, + oauthChannel: entry.oauthChannel, + }); setNotice(t('aliases.deleted', { alias: entry.alias })); await load(); } catch (requestError) { @@ -485,8 +392,6 @@ export function ThinkingAliasesPage() { ) { setSelectedSourceId(''); setEffort(''); - setFastEnabled(false); - setCustomEffortOpen(false); setAlias(''); generatedAliasRef.current = ''; } @@ -572,92 +477,32 @@ export function ThinkingAliasesPage() { className={!normalizedEffort ? 'active none' : 'none'} onClick={() => { chooseEffort(''); - setCustomEffortOpen(false); }} disabled={Boolean(busyAlias)} title={t('aliases.effort.noneHint')} > {t('aliases.effort.none')} - {effortOptions.map((option) => ( + {visibleEffortOptions.map((option) => ( ))} - - {customEffortOpen ?
- chooseEffort(event.currentTarget.value)} - placeholder={t('aliases.customPlaceholder')} - maxLength={64} - spellCheck={false} - autoFocus - disabled={Boolean(busyAlias)} - /> - - {t('aliases.customHelp')} -
: null} {selectedSource && !selectedSource.supportsReasoning ? ( {t('aliases.effort.unsupported')} ) : null} -
-
- {t('aliases.fast.title')} - {t('aliases.fast.description')} -
- -
- {entries.length} + {thinkingEntries.length}
{loading ? (
{t('aliases.loadingConfig')}
- ) : entries.length === 0 ? ( + ) : thinkingEntries.length === 0 ? (
{t('aliases.empty.title')} {t('aliases.empty.description')}
- ) : entries.map((entry) => ( + ) : thinkingEntries.map((entry) => (
@@ -744,9 +584,6 @@ export function ThinkingAliasesPage() { {entry.effort ? ( {entry.effort} ) : null} - {entry.serviceTier ? ( - {t('speedAliases.fast.title')} - ) : null}