diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 5e50161..58f03f4 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -2124,14 +2124,23 @@ async fn create_thinking_alias( return Err("请先选择原模型".to_string()); } let alias = validate_thinking_alias_model_id(&alias, "别名模型")?; - let effort = validate_thinking_alias_effort(&effort)?; + let effort = if effort.trim().is_empty() { + String::new() + } 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_codex_model_definitions(&config) .await .unwrap_or_default(); - let sources = resolved_thinking_alias_sources(&content, &definitions, &available_models)?; + let sources = if effort.is_empty() { + resolved_alias_sources(&content, &definitions, &available_models, false)? + } else { + resolved_thinking_alias_sources(&content, &definitions, &available_models)? + }; let source = sources .iter() .find(|source| source.source.id == source_id) @@ -2158,8 +2167,7 @@ async fn create_thinking_alias( return Err(format!("别名模型 {alias} 已存在")); } - let updated = - add_model_alias_to_yaml(&content, &source, &alias, &effort, fast.unwrap_or(false))?; + let updated = add_model_alias_to_yaml(&content, &source, &alias, &effort, fast)?; put_management_config_yaml(&config, &updated).await?; thinking_aliases_from_yaml(&updated) } @@ -13762,13 +13770,14 @@ fn collect_config_thinking_alias_entries( if source_model == alias { continue; } - let Some(effort) = find_thinking_alias_effort(root, &alias, protocol) else { + let effort = find_thinking_alias_effort(root, &alias, protocol); + if effort.is_none() && find_speed_alias_service_tier(root, &alias, protocol).is_some() { continue; - }; + } entries.push(ThinkingAliasEntry { source_model, alias, - effort: Some(effort), + effort, provider: provider_name.clone(), kind: kind.to_string(), }); @@ -13955,6 +13964,9 @@ fn add_model_alias_to_yaml( } remove_thinking_payload_model(root, alias)?; + if effort.is_empty() && !fast { + return render_updated_core_yaml(&mut document, updated); + } let payload = root .entry(yaml_key("payload")) .or_insert_with(|| serde_norway::Value::Mapping(serde_norway::Mapping::new())) @@ -13976,21 +13988,24 @@ fn add_model_alias_to_yaml( serde_norway::Value::String(source.source.protocol.clone()), ); let mut params_mapping = serde_norway::Mapping::new(); - params_mapping.insert( - yaml_key(if source.source.protocol == "openai" { - "reasoning_effort" - } else { - "reasoning.effort" - }), - serde_norway::Value::String(effort.to_string()), - ); + 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 source.source.protocol == "openai" + if !effort.is_empty() + && source.source.protocol == "openai" && source .source .model @@ -14161,24 +14176,26 @@ fn append_config_thinking_alias( yaml_key("alias"), serde_norway::Value::String(alias.to_string()), ); - if let Some(display_name) = yaml_mapping_value(&alias_model, "display-name") - .and_then(serde_norway::Value::as_str) - .map(str::to_string) - { - alias_model.insert( - yaml_key("display-name"), - serde_norway::Value::String(format!("{display_name} ({effort})")), + if !effort.is_empty() { + if let Some(display_name) = yaml_mapping_value(&alias_model, "display-name") + .and_then(serde_norway::Value::as_str) + .map(str::to_string) + { + alias_model.insert( + yaml_key("display-name"), + 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())]), ); } - 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(()) } @@ -21339,6 +21356,46 @@ oauth-model-alias: ); } + #[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(); + + assert!(rendered.contains("alias: gpt-5.5-alias"), "{rendered}"); + assert!(!rendered.contains("payload:"), "{rendered}"); + assert_eq!( + thinking_aliases_from_yaml(&rendered).unwrap(), + vec![ThinkingAliasEntry { + source_model: "gpt-5.5".to_string(), + alias: "gpt-5.5-alias".to_string(), + effort: None, + provider: "Codex OAuth".to_string(), + kind: "codex-oauth".to_string(), + }] + ); + } + + #[test] + fn configured_model_alias_can_be_created_without_overrides() { + let input = "codex-api-key:\n - api-key: test\n base-url: https://example.com/v1\n models:\n - name: gpt-custom\n"; + let available_models = test_agent_models(&["gpt-custom"]); + let sources = resolved_alias_sources(input, &[], &available_models, false).unwrap(); + let source = sources + .iter() + .find(|source| source.source.model == "gpt-custom") + .unwrap(); + let rendered = + add_model_alias_to_yaml(input, source, "gpt-custom-alias", "", false).unwrap(); + + assert!(rendered.contains("alias: gpt-custom-alias"), "{rendered}"); + assert!(!rendered.contains("thinking:"), "{rendered}"); + assert!(!rendered.contains("payload:"), "{rendered}"); + let entries = thinking_aliases_from_yaml(&rendered).unwrap(); + assert_eq!(entries.len(), 1); + assert_eq!(entries[0].effort, None); + } + #[test] fn model_alias_can_apply_reasoning_and_fast_together() { let source = test_codex_oauth_thinking_source("gpt-5.6-sol"); diff --git a/src/pages/ThinkingAliasesPage.tsx b/src/pages/ThinkingAliasesPage.tsx index 33f1828..3714ca1 100644 --- a/src/pages/ThinkingAliasesPage.tsx +++ b/src/pages/ThinkingAliasesPage.tsx @@ -116,7 +116,8 @@ export const defaultModelAlias = ( ) => { const normalizedModel = model?.trim() ?? ''; const normalizedEffort = effort.trim().toLowerCase(); - if (!normalizedModel || (!normalizedEffort && !fast)) return ''; + if (!normalizedModel) return ''; + if (!normalizedEffort && !fast) return `${normalizedModel}-alias`; return `${normalizedModel}${normalizedEffort ? `-${normalizedEffort}` : ''}${fast ? '-fast' : ''}`; }; @@ -273,10 +274,10 @@ export function ThinkingAliasesPage() { setSelectedSourceId(source.id); if (!source.supportsReasoning) { setEffort(''); - setFastEnabled(source.supportsFast); + setFastEnabled(source.supportsFast && (fastEnabled || !source.supportsReasoning)); } else { setEffort(''); - setFastEnabled(false); + setFastEnabled(source.supportsFast && fastEnabled); } setCustomEffortOpen(false); setAlias(''); @@ -343,10 +344,6 @@ export function ThinkingAliasesPage() { setError(t('aliases.error.emptyAlias')); return; } - if (!normalizedEffort && !fastEnabled) { - setError(t('aliases.error.emptyOptions')); - return; - } if (fastEnabled && !selectedSource.supportsFast) { setError(t('aliases.error.unsupportedFast')); return; @@ -371,12 +368,23 @@ export function ThinkingAliasesPage() { alias: normalizedAlias, effort: normalizedEffort, })); - } else { + } 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'), + })); } setAlias(''); await load(); @@ -397,7 +405,7 @@ export function ThinkingAliasesPage() { setError(''); setNotice(''); try { - if (entry.effort) { + if (entry.effort || !entry.serviceTier) { await invoke('delete_thinking_alias', { alias: entry.alias, }); @@ -629,7 +637,7 @@ export function ThinkingAliasesPage() { type="checkbox" checked={fastEnabled} onChange={(event) => setFastEnabled(event.currentTarget.checked)} - disabled={Boolean(busyAlias) || !selectedSource || !selectedSource.supportsFast} + disabled={Boolean(busyAlias) || Boolean(selectedSource && !selectedSource.supportsFast)} aria-label={t('aliases.fast.title')} /> @@ -651,7 +659,9 @@ export function ThinkingAliasesPage() { placeholder={selectedSource ? normalizedEffort ? t('aliases.aliasName.example', { model: selectedSource.model, effort: normalizedEffort }) - : t('speedAliases.aliasName.example', { model: selectedSource.model }) + : fastEnabled + ? t('speedAliases.aliasName.example', { model: selectedSource.model }) + : t('aliases.aliasName.example', { model: selectedSource.model, effort: 'alias' }) : t('aliases.aliasName.selectFirst')} disabled={Boolean(busyAlias)} /> @@ -674,7 +684,7 @@ export function ThinkingAliasesPage() { type="button" className="primary-button thinking-alias-create" onClick={() => void createAlias()} - disabled={loading || Boolean(busyAlias) || !selectedSource || (!normalizedEffort && !fastEnabled) || !alias.trim()} + disabled={loading || Boolean(busyAlias)} > {busyAction === 'create' ? diff --git a/tests/thinkingAliases.test.ts b/tests/thinkingAliases.test.ts index d2da751..40c190a 100644 --- a/tests/thinkingAliases.test.ts +++ b/tests/thinkingAliases.test.ts @@ -12,7 +12,7 @@ describe('模型别名默认名称', () => { expect(defaultModelAlias('gpt-5.6-sol', 'XHigh', false)).toBe('gpt-5.6-sol-xhigh'); expect(defaultModelAlias('gpt-5.6-sol', '', true)).toBe('gpt-5.6-sol-fast'); expect(defaultModelAlias('gpt-5.6-sol', 'xhigh', true)).toBe('gpt-5.6-sol-xhigh-fast'); - expect(defaultModelAlias('gpt-5.6-sol', '', false)).toBe(''); + expect(defaultModelAlias('gpt-5.6-sol', '', false)).toBe('gpt-5.6-sol-alias'); }); test('默认别名与现有模型重名时递增数字后缀', () => {