diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 86828e3..4565458 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1820,6 +1820,7 @@ 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(); @@ -1861,7 +1862,8 @@ async fn create_thinking_alias( return Err(format!("别名模型 {alias} 已存在")); } - let updated = add_thinking_alias_to_yaml(&content, &source, &alias, &effort)?; + let updated = + add_model_alias_to_yaml(&content, &source, &alias, &effort, fast.unwrap_or(false))?; put_management_config_yaml(&config, &updated).await?; thinking_aliases_from_yaml(&updated) } @@ -12230,6 +12232,16 @@ fn add_thinking_alias_to_yaml( source: &ResolvedThinkingAliasSource, alias: &str, effort: &str, +) -> Result { + add_model_alias_to_yaml(content, source, alias, effort, false) +} + +fn add_model_alias_to_yaml( + content: &str, + source: &ResolvedThinkingAliasSource, + alias: &str, + effort: &str, + fast: bool, ) -> Result { let mut document = yaml_serde_edit::YamlValue::parse(content) .map_err(|error| format!("解析内核 YAML 配置失败: {error}"))?; @@ -12291,6 +12303,12 @@ fn add_thinking_alias_to_yaml( }), 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" && source .source @@ -12581,6 +12599,7 @@ fn remove_thinking_alias_from_yaml(content: &str, alias: &str) -> Result = { '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.created': 'Created {alias} with reasoning effort fixed to {effort}', - 'aliases.deleteConfirm': 'Delete the model alias “{alias}”?\nIts reasoning-effort override will also be removed.', + '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.deleted': 'Deleted {alias}', 'aliases.create.title': 'Create Model Alias', - 'aliases.create.description': 'Configure reasoning effort or the Fast service tier in one place', + 'aliases.create.description': 'Reasoning effort and the Fast service tier can be configured independently or together', 'aliases.originalModel': 'Original Model', 'aliases.loadingModels': 'Loading models', 'aliases.searchModel': 'Search and select an original model', @@ -1100,9 +1112,15 @@ export const en: Record = { '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.effort.title': 'Alias Option', - 'aliases.effort.description': 'Choose reasoning effort, a custom level, or the Fast service tier', + 'aliases.effort.title': 'Reasoning Effort', + 'aliases.effort.description': 'Configure it independently or use it together with Fast', + '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', @@ -1119,7 +1137,7 @@ export const en: Record = { 'aliases.creating': 'Creating', 'aliases.create': 'Create Alias', 'aliases.createdList.title': 'Created Aliases', - 'aliases.createdList.description': 'Reasoning and Fast aliases are managed in one list', + 'aliases.createdList.description': 'Each alias shows its reasoning effort and Fast status together', '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', diff --git a/src/pages/ThinkingAliasesPage.tsx b/src/pages/ThinkingAliasesPage.tsx index 792abdd..5c16f71 100644 --- a/src/pages/ThinkingAliasesPage.tsx +++ b/src/pages/ThinkingAliasesPage.tsx @@ -38,11 +38,9 @@ type SpeedAliasEntry = { kind: string; }; -type AliasListEntry = - | (ThinkingAliasEntry & { mode: 'reasoning' }) - | (SpeedAliasEntry & { mode: 'fast' }); - -type AliasMode = 'reasoning' | 'fast'; +type AliasListEntry = ThinkingAliasEntry & { + serviceTier: string | null; +}; type ThinkingAliasSource = { id: string; @@ -53,6 +51,10 @@ type ThinkingAliasSource = { protocol: string; }; +type ModelAliasSource = ThinkingAliasSource & { + supportsReasoning: boolean; +}; + const effortOptions = [ { value: 'low', label: 'Low', hintKey: 'aliases.effort.low' }, { value: 'medium', label: 'Medium', hintKey: 'aliases.effort.medium' }, @@ -64,13 +66,45 @@ const effortOptions = [ export const combineModelAliasEntries = ( thinkingEntries: ThinkingAliasEntry[], speedEntries: SpeedAliasEntry[], -): AliasListEntry[] => [ - ...thinkingEntries.map((entry) => ({ ...entry, mode: 'reasoning' as const })), - ...speedEntries.map((entry) => ({ ...entry, mode: 'fast' as const })), -].sort((left, right) => ( - left.provider.localeCompare(right.provider) - || left.alias.localeCompare(right.alias) -)); +): AliasListEntry[] => { + const entries = new Map(); + const entryKey = (entry: Pick) => ( + [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 = ( + thinkingSources: ThinkingAliasSource[], + speedSources: ThinkingAliasSource[], +): ModelAliasSource[] => { + const reasoningSourceIds = new Set(thinkingSources.map((source) => source.id)); + const sources = new Map(); + [...speedSources, ...thinkingSources].forEach((source) => { + sources.set(source.id, { + ...source, + supportsReasoning: reasoningSourceIds.has(source.id), + }); + }); + return [...sources.values()]; +}; export const thinkingAliasSourceKindLabel = (kind: string) => { if (kind === 'codex-oauth') return 'Codex OAuth'; @@ -96,8 +130,8 @@ export function ThinkingAliasesPage() { const [thinkingSources, setThinkingSources] = useState([]); const [speedSources, setSpeedSources] = useState([]); const [selectedSourceId, setSelectedSourceId] = useState(''); - const [aliasMode, setAliasMode] = useState('reasoning'); const [effort, setEffort] = useState('xhigh'); + const [fastEnabled, setFastEnabled] = useState(false); const [customEffortOpen, setCustomEffortOpen] = useState(false); const [alias, setAlias] = useState(''); const [search, setSearch] = useState(''); @@ -152,7 +186,10 @@ export function ThinkingAliasesPage() { return () => document.removeEventListener('pointerdown', closeOnOutsideClick); }, [modelPickerOpen]); - const sources = aliasMode === 'fast' ? speedSources : thinkingSources; + const sources = useMemo( + () => combineModelAliasSources(thinkingSources, speedSources), + [speedSources, thinkingSources], + ); const entries = useMemo( () => combineModelAliasEntries(thinkingEntries, speedEntries), [speedEntries, thinkingEntries], @@ -195,8 +232,13 @@ export function ThinkingAliasesPage() { setActiveSourceIndex(0); }, [search, sources]); - const chooseSource = (source: ThinkingAliasSource) => { + const chooseSource = (source: ModelAliasSource) => { setSelectedSourceId(source.id); + if (!source.supportsReasoning) { + setEffort(''); + setFastEnabled(true); + setCustomEffortOpen(false); + } setModelPickerOpen(false); setSearch(''); }; @@ -233,8 +275,7 @@ export function ThinkingAliasesPage() { const normalizedEffort = effort.trim().toLowerCase(); const customEffortSelected = Boolean( - aliasMode === 'reasoning' - && normalizedEffort + normalizedEffort && !effortOptions.some((option) => option.value === normalizedEffort), ); @@ -248,8 +289,12 @@ export function ThinkingAliasesPage() { setError(t('aliases.error.emptyAlias')); return; } - if (aliasMode === 'reasoning' && !normalizedEffort) { - setError(t('aliases.error.emptyEffort')); + if (!normalizedEffort && !fastEnabled) { + setError(t('aliases.error.emptyOptions')); + return; + } + if (normalizedEffort && !selectedSource.supportsReasoning) { + setError(t('aliases.error.unsupportedEffort')); return; } setBusyAlias(normalizedAlias); @@ -257,23 +302,26 @@ export function ThinkingAliasesPage() { setError(''); setNotice(''); try { - if (aliasMode === 'fast') { - const nextEntries = await invoke('create_speed_alias', { - sourceId: selectedSource.id, - alias: normalizedAlias, - }); - setSpeedEntries(nextEntries); - setNotice(t('speedAliases.created', { alias: normalizedAlias })); - } else { - const nextEntries = await invoke('create_thinking_alias', { + if (normalizedEffort) { + await invoke('create_thinking_alias', { sourceId: selectedSource.id, alias: normalizedAlias, effort: normalizedEffort, + fast: fastEnabled, }); - setThinkingEntries(nextEntries); - setNotice(t('aliases.created', { alias: normalizedAlias, effort: normalizedEffort })); + setNotice(t(fastEnabled ? 'aliases.createdCombined' : 'aliases.created', { + alias: normalizedAlias, + effort: normalizedEffort, + })); + } else { + await invoke('create_speed_alias', { + sourceId: selectedSource.id, + alias: normalizedAlias, + }); + setNotice(t('speedAliases.created', { alias: normalizedAlias })); } setAlias(''); + await load(); } catch (requestError) { setError(String(requestError)); } finally { @@ -284,28 +332,24 @@ export function ThinkingAliasesPage() { const deleteAlias = async (entry: AliasListEntry) => { if (!window.confirm( - t(entry.mode === 'fast' ? 'speedAliases.deleteConfirm' : 'aliases.deleteConfirm', { - alias: entry.alias, - }), + t('aliases.deleteConfirm', { alias: entry.alias }), )) return; setBusyAlias(entry.alias); setBusyAction('delete'); setError(''); setNotice(''); try { - if (entry.mode === 'fast') { - const nextEntries = await invoke('delete_speed_alias', { + if (entry.effort) { + await invoke('delete_thinking_alias', { alias: entry.alias, }); - setSpeedEntries(nextEntries); - setNotice(t('speedAliases.deleted', { alias: entry.alias })); } else { - const nextEntries = await invoke('delete_thinking_alias', { + await invoke('delete_speed_alias', { alias: entry.alias, }); - setThinkingEntries(nextEntries); - setNotice(t('aliases.deleted', { alias: entry.alias })); } + setNotice(t('aliases.deleted', { alias: entry.alias })); + await load(); } catch (requestError) { setError(String(requestError)); } finally { @@ -431,43 +475,41 @@ export function ThinkingAliasesPage() { {t('aliases.effort.description')}
+ {effortOptions.map((option) => ( ))} - {t('aliases.customHelp')}
: null} + {selectedSource && !selectedSource.supportsReasoning ? ( + {t('aliases.effort.unsupported')} + ) : null} + + +
+
+ {t('aliases.fast.title')} + {t('aliases.fast.description')} +
+
- {aliasMode === 'fast' ? : } + {normalizedEffort ? : }
{selectedSource?.model || t('aliases.notSelected')} {alias || t('aliases.enterAlias')} - {aliasMode === 'fast' - ? 'service_tier = priority' - : `${selectedSource?.protocol === 'openai' ? 'reasoning_effort' : 'reasoning.effort'} = ${normalizedEffort || t('aliases.notSet')}`} + {[ + normalizedEffort + ? `${selectedSource?.protocol === 'openai' ? 'reasoning_effort' : 'reasoning.effort'} = ${normalizedEffort}` + : '', + fastEnabled ? 'service_tier = priority' : '', + ].filter(Boolean).join(' · ') || t('aliases.notSet')}
@@ -536,11 +598,11 @@ export function ThinkingAliasesPage() { type="button" className="primary-button thinking-alias-create" onClick={() => void createAlias()} - disabled={loading || Boolean(busyAlias) || !selectedSource || (aliasMode === 'reasoning' && !effort.trim()) || !alias.trim()} + disabled={loading || Boolean(busyAlias) || !selectedSource || (!normalizedEffort && !fastEnabled) || !alias.trim()} > {busyAction === 'create' ? - : aliasMode === 'fast' ? : } + : } {busyAction === 'create' ? t('aliases.creating') : t('aliases.create')} @@ -578,19 +640,20 @@ export function ThinkingAliasesPage() { {entry.alias} - - {entry.mode === 'fast' - ? t('speedAliases.fast.title') - : entry.effort ?? t('aliases.unboundEffort')} - +
+ {entry.effort ? ( + {entry.effort} + ) : null} + {entry.serviceTier ? ( + {t('speedAliases.fast.title')} + ) : null} +