From aa8a7cbd84dd0fc8fdeb394b644e3c7b6ad61dd9 Mon Sep 17 00:00:00 2001 From: lzt404 <2596933790@qq.com> Date: Sun, 2 Aug 2026 14:47:20 +0800 Subject: [PATCH] feat(aliases): integrate Fast options into model alias management --- src/i18n/ja.ts | 24 +- src/i18n/resources.ts | 48 ++-- src/pages/ConfigPanel.tsx | 26 +- src/pages/SpeedAliasesPage.tsx | 399 ------------------------------ src/pages/ThinkingAliasesPage.tsx | 159 +++++++++--- src/styles.css | 21 +- tests/thinkingAliases.test.ts | 31 ++- 7 files changed, 203 insertions(+), 505 deletions(-) delete mode 100644 src/pages/SpeedAliasesPage.tsx diff --git a/src/i18n/ja.ts b/src/i18n/ja.ts index f145c92..22075a9 100644 --- a/src/i18n/ja.ts +++ b/src/i18n/ja.ts @@ -13,7 +13,6 @@ export const jaOverrides = { 'app.nav.kernel': 'コア', 'app.nav.config': '詳細設定', 'app.nav.thinkingAliases': 'モデルエイリアス', - 'app.nav.speedAliases': '速度エイリアス', 'app.nav.oauth': 'OAuth', 'app.nav.api': 'API 接続', 'app.nav.authFiles': '認証ファイル', @@ -207,8 +206,8 @@ export const jaOverrides = { 'aliases.created': '{alias} を作成し、推論レベルを {effort} に固定しました', 'aliases.deleteConfirm': 'エイリアスモデル「{alias}」を削除しますか?\n対応する推論レベルの上書きルールも削除されます。', 'aliases.deleted': '{alias} を削除しました', - 'aliases.create.title': '推論エイリアスを作成', - 'aliases.create.description': 'Codex OAuth、Codex API、OpenAI 互換接続に対応', + 'aliases.create.title': 'モデルエイリアスを作成', + 'aliases.create.description': '推論レベルと Fast 速度階層を一か所で設定します', 'aliases.originalModel': '元のモデル', 'aliases.loadingModels': 'モデルを読み込んでいます', 'aliases.searchModel': '元のモデルを検索して選択', @@ -217,8 +216,9 @@ export const jaOverrides = { 'aliases.noModels': '選択可能なモデルがありません', 'aliases.sourceLabel': 'ソース:{source}', 'aliases.sourceHint': '同名モデルは Codex OAuth、Codex API、OpenAI 互換ソースごとに表示されます', - 'aliases.effort.title': '推論レベル', - 'aliases.effort.description': '一般的なレベルを選択するか、上流が対応するカスタム値を入力します', + 'aliases.effort.title': 'エイリアスオプション', + 'aliases.effort.description': '推論レベル、カスタム値、または Fast 速度階層を選択します', + 'aliases.fast.description': 'Fast リクエスト階層を固定します(service_tier=priority)', 'aliases.customLevel': 'カスタムレベル:{effort}', 'aliases.customLevelHint': 'モデルプロバイダーが対応する別の推論レベルを入力', 'aliases.custom': 'カスタム', @@ -235,25 +235,17 @@ export const jaOverrides = { 'aliases.creating': '作成中', 'aliases.create': 'エイリアスを作成', 'aliases.createdList.title': '作成済みエイリアス', - 'aliases.createdList.description': '各エイリアスには元のモデル、ソース、固定推論レベルが保持されます', + 'aliases.createdList.description': '推論エイリアスと Fast エイリアスを一つの一覧で管理します', 'aliases.loadingConfig': '設定を読み込んでいます', - 'aliases.empty.title': '推論エイリアスはまだありません', - 'aliases.empty.description': '左側でモデルとレベルを選択して作成できます', + 'aliases.empty.title': 'モデルエイリアスはまだありません', + 'aliases.empty.description': '左側でモデルとエイリアスオプションを選択して作成できます', 'aliases.unboundEffort': 'レベル未設定', 'aliases.delete': '{alias} を削除', 'speedAliases.created': '{alias} を作成し、速度階層を Fast に固定しました', 'speedAliases.deleted': '{alias} を削除しました', 'speedAliases.deleteConfirm': '速度エイリアス「{alias}」を削除しますか?\n対応する Fast 上書きルールも削除されます。', - 'speedAliases.create.title': '速度エイリアスを作成', - 'speedAliases.create.description': '常に Fast リクエスト階層を使用するエイリアスを作成します', 'speedAliases.fast.title': 'Fast', 'speedAliases.aliasName.example': '例:{model}-fast', - 'speedAliases.creating': '作成中', - 'speedAliases.create': 'Fast エイリアスを作成', - 'speedAliases.createdList.title': '作成済み速度エイリアス', - 'speedAliases.createdList.description': '各エイリアスには元のモデル、プロバイダー、固定 Fast 階層が保持されます', - 'speedAliases.empty.title': '速度エイリアスはまだありません', - 'speedAliases.empty.description': '左側でモデルを選択して Fast エイリアスを作成できます', 'speedAliases.delete': '{alias} を削除', 'config.routing.roundRobin': 'ラウンドロビン', 'config.routing.fillFirst': '優先充填', diff --git a/src/i18n/resources.ts b/src/i18n/resources.ts index 03df35a..7c72192 100644 --- a/src/i18n/resources.ts +++ b/src/i18n/resources.ts @@ -16,7 +16,6 @@ export const zhCN = { 'app.nav.kernel': '内核', 'app.nav.config': '高级设置', 'app.nav.thinkingAliases': '模型别名', - 'app.nav.speedAliases': '速度别名', 'app.nav.oauth': 'OAuth', 'app.nav.api': 'API 接入', 'app.nav.authFiles': '认证文件', @@ -210,8 +209,8 @@ export const zhCN = { 'aliases.created': '已创建 {alias},思考强度固定为 {effort}', 'aliases.deleteConfirm': '确定删除别名模型“{alias}”吗?\n将同时移除对应的思考强度覆盖规则。', 'aliases.deleted': '已删除 {alias}', - 'aliases.create.title': '创建思考别名', - 'aliases.create.description': '支持 Codex OAuth、Codex API 和 OpenAI 兼容接入', + 'aliases.create.title': '创建模型别名', + 'aliases.create.description': '统一设置思考强度或 Fast 速度层级', 'aliases.originalModel': '原模型', 'aliases.loadingModels': '正在读取模型', 'aliases.searchModel': '搜索并选择原模型', @@ -220,8 +219,9 @@ export const zhCN = { 'aliases.noModels': '当前没有可选模型', 'aliases.sourceLabel': '来源:{source}', 'aliases.sourceHint': '同名模型会按 Codex OAuth、Codex API 和 OpenAI 兼容来源分别列出', - 'aliases.effort.title': '思考强度', - 'aliases.effort.description': '选择常用等级,或输入上游支持的自定义值', + 'aliases.effort.title': '别名选项', + 'aliases.effort.description': '选择思考强度、自定义等级或 Fast 速度层级', + 'aliases.fast.description': '固定使用 Fast 请求层级(service_tier=priority)', 'aliases.customLevel': '自定义等级:{effort}', 'aliases.customLevelHint': '输入模型提供商支持的其他思考等级', 'aliases.custom': '自定义', @@ -238,25 +238,17 @@ export const zhCN = { 'aliases.creating': '创建中', 'aliases.create': '创建别名', 'aliases.createdList.title': '已创建别名', - 'aliases.createdList.description': '每个别名都保留原模型、来源和固定思考强度', + 'aliases.createdList.description': '统一显示思考强度别名和 Fast 速度别名', 'aliases.loadingConfig': '读取配置中', - 'aliases.empty.title': '尚未创建思考别名', - 'aliases.empty.description': '从左侧选择模型和强度即可创建', + 'aliases.empty.title': '尚未创建模型别名', + 'aliases.empty.description': '从左侧选择模型和别名选项即可创建', 'aliases.unboundEffort': '未绑定强度', 'aliases.delete': '删除 {alias}', 'speedAliases.created': '已创建 {alias},速度层级固定为 Fast', 'speedAliases.deleted': '已删除 {alias}', 'speedAliases.deleteConfirm': '确定删除速度别名模型“{alias}”吗?\n将同时移除对应的 Fast 覆盖规则。', - 'speedAliases.create.title': '创建速度别名', - 'speedAliases.create.description': '为别名模型固定启用 Fast 请求层级', 'speedAliases.fast.title': 'Fast', 'speedAliases.aliasName.example': '例如 {model}-fast', - 'speedAliases.creating': '创建中', - 'speedAliases.create': '创建 Fast 别名', - 'speedAliases.createdList.title': '已创建速度别名', - 'speedAliases.createdList.description': '每个别名都保留原模型、来源和固定 Fast 层级', - 'speedAliases.empty.title': '尚未创建速度别名', - 'speedAliases.empty.description': '从左侧选择模型即可创建 Fast 别名', 'speedAliases.delete': '删除 {alias}', 'config.routing.roundRobin': '轮询', 'config.routing.fillFirst': '优先填充', @@ -905,7 +897,6 @@ export const en: Record = { 'app.nav.kernel': 'Core', 'app.nav.config': 'Advanced Settings', 'app.nav.thinkingAliases': 'Model Aliases', - 'app.nav.speedAliases': 'Speed Aliases', 'app.nav.oauth': 'OAuth', 'app.nav.api': 'API Access', 'app.nav.authFiles': 'Auth Files', @@ -1099,8 +1090,8 @@ export const en: Record = { '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.deleted': 'Deleted {alias}', - 'aliases.create.title': 'Create Reasoning Alias', - 'aliases.create.description': 'Supports Codex OAuth, Codex API, and OpenAI-compatible connections', + 'aliases.create.title': 'Create Model Alias', + 'aliases.create.description': 'Configure reasoning effort or the Fast service tier in one place', 'aliases.originalModel': 'Original Model', 'aliases.loadingModels': 'Loading models', 'aliases.searchModel': 'Search and select an original model', @@ -1109,8 +1100,9 @@ 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': 'Reasoning Effort', - 'aliases.effort.description': 'Choose a common level or enter a custom value supported by the upstream provider', + 'aliases.effort.title': 'Alias Option', + 'aliases.effort.description': 'Choose reasoning effort, a custom level, or the Fast service tier', + 'aliases.fast.description': 'Always use the Fast request tier (service_tier=priority)', 'aliases.customLevel': 'Custom level: {effort}', 'aliases.customLevelHint': 'Enter another reasoning level supported by the model provider', 'aliases.custom': 'Custom', @@ -1127,25 +1119,17 @@ export const en: Record = { 'aliases.creating': 'Creating', 'aliases.create': 'Create Alias', 'aliases.createdList.title': 'Created Aliases', - 'aliases.createdList.description': 'Each alias retains its original model, source, and fixed reasoning effort', + 'aliases.createdList.description': 'Reasoning and Fast aliases are managed in one list', 'aliases.loadingConfig': 'Loading configuration', - 'aliases.empty.title': 'No reasoning aliases yet', - 'aliases.empty.description': 'Select a model and effort on the left to create one', + '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.create.title': 'Create Speed Alias', - 'speedAliases.create.description': 'Create an alias that always enables the Fast request tier', 'speedAliases.fast.title': 'Fast', 'speedAliases.aliasName.example': 'For example: {model}-fast', - 'speedAliases.creating': 'Creating', - 'speedAliases.create': 'Create Fast Alias', - 'speedAliases.createdList.title': 'Created Speed Aliases', - 'speedAliases.createdList.description': 'Each alias retains its source model, provider, and fixed Fast tier', - 'speedAliases.empty.title': 'No speed aliases yet', - 'speedAliases.empty.description': 'Select a model on the left to create a Fast alias', 'speedAliases.delete': 'Delete {alias}', 'config.routing.roundRobin': 'Round Robin', 'config.routing.fillFirst': 'Fill First', diff --git a/src/pages/ConfigPanel.tsx b/src/pages/ConfigPanel.tsx index 6d3e448..d1d35bb 100644 --- a/src/pages/ConfigPanel.tsx +++ b/src/pages/ConfigPanel.tsx @@ -22,7 +22,6 @@ import { } from 'lucide-react'; import { useCoreRuntime, type CoreStatus } from '../coreRuntime'; import { useI18n } from '../i18n'; -import { SpeedAliasesPage } from './SpeedAliasesPage'; import { ThinkingAliasesPage } from './ThinkingAliasesPage'; type CoreConfigSettings = { @@ -50,7 +49,7 @@ type ConfigAction = | 'network' | null; type NoticeTone = 'success' | 'error'; -type ConfigSubpage = 'general' | 'network' | 'aliases' | 'speedAliases'; +type ConfigSubpage = 'general' | 'network' | 'aliases'; const ROUTING_OPTIONS = [ { value: 'round-robin', labelKey: 'config.routing.roundRobin' }, @@ -458,18 +457,6 @@ export function ConfigPanelPage() { > {t('app.nav.thinkingAliases')} - {activeSubpage === 'general' ? ( @@ -888,7 +875,7 @@ export function ConfigPanelPage() { - ) : activeSubpage === 'aliases' ? ( + ) : (
- ) : ( -
- -
)} {addDialogOpen ? ( diff --git a/src/pages/SpeedAliasesPage.tsx b/src/pages/SpeedAliasesPage.tsx deleted file mode 100644 index 10f84c0..0000000 --- a/src/pages/SpeedAliasesPage.tsx +++ /dev/null @@ -1,399 +0,0 @@ -import { - type KeyboardEvent, - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from 'react'; -import { invoke } from '@tauri-apps/api/core'; -import { - ArrowRight, - Check, - GitFork, - LoaderCircle, - Search, - Trash2, - Zap, -} from 'lucide-react'; -import { useI18n } from '../i18n'; -import { thinkingAliasSourceKindLabel } from './ThinkingAliasesPage'; - -type SpeedAliasEntry = { - sourceModel: string; - alias: string; - serviceTier: string; - provider: string; - kind: string; -}; - -type SpeedAliasSource = { - id: string; - model: string; - displayName: string | null; - provider: string; - kind: string; - protocol: string; -}; - -export function SpeedAliasesPage() { - const { t } = useI18n(); - const [entries, setEntries] = useState([]); - const [sources, setSources] = useState([]); - const [selectedSourceId, setSelectedSourceId] = useState(''); - const [alias, setAlias] = useState(''); - const [search, setSearch] = useState(''); - const [modelPickerOpen, setModelPickerOpen] = useState(false); - const [activeSourceIndex, setActiveSourceIndex] = useState(0); - const modelPickerRef = useRef(null); - const [loading, setLoading] = useState(true); - const [busyAlias, setBusyAlias] = useState(''); - const [busyAction, setBusyAction] = useState<'create' | 'delete' | ''>(''); - const [error, setError] = useState(''); - const [notice, setNotice] = useState(''); - - const load = useCallback(async () => { - setLoading(true); - setError(''); - try { - const [nextEntries, nextSources] = await Promise.all([ - invoke('get_speed_aliases'), - invoke('get_speed_alias_sources'), - ]); - setEntries(nextEntries); - setSources(nextSources); - setSelectedSourceId((current) => ( - nextSources.some((source) => source.id === current) ? current : '' - )); - } catch (requestError) { - setError(String(requestError)); - } finally { - setLoading(false); - } - }, []); - - useEffect(() => { - void load(); - }, [load]); - - useEffect(() => { - if (!modelPickerOpen) return undefined; - const closeOnOutsideClick = (event: PointerEvent) => { - if (!modelPickerRef.current?.contains(event.target as Node)) { - setModelPickerOpen(false); - setSearch(''); - } - }; - document.addEventListener('pointerdown', closeOnOutsideClick); - return () => document.removeEventListener('pointerdown', closeOnOutsideClick); - }, [modelPickerOpen]); - - const selectedSource = useMemo( - () => sources.find((source) => source.id === selectedSourceId) ?? null, - [selectedSourceId, sources], - ); - - const filteredSources = useMemo(() => { - const query = search.trim().toLowerCase(); - if (!query) return sources; - return sources.filter((source) => { - const haystack = [ - source.model, - source.displayName ?? '', - source.provider, - thinkingAliasSourceKindLabel(source.kind), - ].join(' ').toLowerCase(); - return haystack.includes(query); - }); - }, [search, sources]); - - useEffect(() => { - setActiveSourceIndex(0); - }, [search, sources]); - - const chooseSource = (source: SpeedAliasSource) => { - setSelectedSourceId(source.id); - setModelPickerOpen(false); - setSearch(''); - }; - - const handleModelSearchKeyDown = (event: KeyboardEvent) => { - if (event.key === 'Escape') { - setModelPickerOpen(false); - setSearch(''); - event.currentTarget.blur(); - return; - } - if (event.key === 'ArrowDown' || event.key === 'ArrowUp') { - event.preventDefault(); - if (!modelPickerOpen) { - setModelPickerOpen(true); - return; - } - if (!filteredSources.length) return; - const direction = event.key === 'ArrowDown' ? 1 : -1; - setActiveSourceIndex((current) => ( - (current + direction + filteredSources.length) % filteredSources.length - )); - return; - } - if (event.key === 'Enter' && modelPickerOpen && filteredSources[activeSourceIndex]) { - event.preventDefault(); - chooseSource(filteredSources[activeSourceIndex]); - } - }; - - const createAlias = async () => { - if (!selectedSource) { - setError(t('aliases.error.selectModel')); - return; - } - const normalizedAlias = alias.trim(); - if (!normalizedAlias) { - setError(t('aliases.error.emptyAlias')); - return; - } - setBusyAlias(normalizedAlias); - setBusyAction('create'); - setError(''); - setNotice(''); - try { - const nextEntries = await invoke('create_speed_alias', { - sourceId: selectedSource.id, - alias: normalizedAlias, - }); - setEntries(nextEntries); - setNotice(t('speedAliases.created', { alias: normalizedAlias })); - setAlias(''); - } catch (requestError) { - setError(String(requestError)); - } finally { - setBusyAlias(''); - setBusyAction(''); - } - }; - - const deleteAlias = async (entry: SpeedAliasEntry) => { - if (!window.confirm(t('speedAliases.deleteConfirm', { alias: entry.alias }))) return; - setBusyAlias(entry.alias); - setBusyAction('delete'); - setError(''); - setNotice(''); - try { - const nextEntries = await invoke('delete_speed_alias', { - alias: entry.alias, - }); - setEntries(nextEntries); - setNotice(t('speedAliases.deleted', { alias: entry.alias })); - } catch (requestError) { - setError(String(requestError)); - } finally { - setBusyAlias(''); - setBusyAction(''); - } - }; - - return ( -
-
- {error ?
{error}
: null} - {!error && notice ?
{notice}
: null} -
- -
-
-
- -
-

{t('speedAliases.create.title')}

-

{t('speedAliases.create.description')}

-
-
- -
- -
-
- {loading ? : } - { - setSearch(selectedSource?.model ?? ''); - setModelPickerOpen(true); - event.currentTarget.select(); - }} - onChange={(event) => { - setSearch(event.currentTarget.value); - setModelPickerOpen(true); - }} - onKeyDown={handleModelSearchKeyDown} - placeholder={loading ? t('aliases.loadingModels') : t('aliases.searchModel')} - autoComplete="off" - spellCheck={false} - disabled={loading} - /> - {!modelPickerOpen && selectedSource ? ( - - {thinkingAliasSourceKindLabel(selectedSource.kind)} - - ) : null} -
- {modelPickerOpen ? ( -
- {filteredSources.length === 0 ? ( -
- {sources.length ? t('aliases.noMatch') : t('aliases.noModels')} -
- ) : filteredSources.map((source, index) => { - const selected = source.id === selectedSourceId; - return ( - - ); - })} -
- ) : null} -
- {selectedSource ? ( -
- {thinkingAliasSourceKindLabel(selectedSource.kind)} - {selectedSource.provider} -
- ) : ( - {t('aliases.sourceHint')} - )} -
- -
- -
- {t('speedAliases.fast.title')} - service_tier = priority -
-
- -
- -
-
-
-

{t('speedAliases.createdList.title')}

- {t('speedAliases.createdList.description')} -
- {entries.length} -
- -
- {loading ? ( -
{t('aliases.loadingConfig')}
- ) : entries.length === 0 ? ( -
- - {t('speedAliases.empty.title')} - {t('speedAliases.empty.description')} -
- ) : entries.map((entry) => ( -
-
-
- {entry.sourceModel} - - {thinkingAliasSourceKindLabel(entry.kind)} - {entry.provider} - -
- - {entry.alias} -
- {entry.serviceTier} - -
- ))} -
-
-
-
- ); -} diff --git a/src/pages/ThinkingAliasesPage.tsx b/src/pages/ThinkingAliasesPage.tsx index 79744cb..792abdd 100644 --- a/src/pages/ThinkingAliasesPage.tsx +++ b/src/pages/ThinkingAliasesPage.tsx @@ -16,6 +16,7 @@ import { Search, Trash2, X, + Zap, } from 'lucide-react'; import { getCurrentLocale, translate, useI18n } from '../i18n'; @@ -29,6 +30,20 @@ type ThinkingAliasEntry = { kind: string; }; +type SpeedAliasEntry = { + sourceModel: string; + alias: string; + serviceTier: string; + provider: string; + kind: string; +}; + +type AliasListEntry = + | (ThinkingAliasEntry & { mode: 'reasoning' }) + | (SpeedAliasEntry & { mode: 'fast' }); + +type AliasMode = 'reasoning' | 'fast'; + type ThinkingAliasSource = { id: string; model: string; @@ -46,6 +61,17 @@ 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[] => [ + ...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) +)); + export const thinkingAliasSourceKindLabel = (kind: string) => { if (kind === 'codex-oauth') return 'Codex OAuth'; if (kind === 'codex-api') return 'Codex API'; @@ -65,9 +91,12 @@ const thinkingAliasSourceDetail = (source: ThinkingAliasSource) => ( export function ThinkingAliasesPage() { const { t } = useI18n(); - const [entries, setEntries] = useState([]); - const [sources, setSources] = useState([]); + const [thinkingEntries, setThinkingEntries] = useState([]); + const [speedEntries, setSpeedEntries] = useState([]); + const [thinkingSources, setThinkingSources] = useState([]); + const [speedSources, setSpeedSources] = useState([]); const [selectedSourceId, setSelectedSourceId] = useState(''); + const [aliasMode, setAliasMode] = useState('reasoning'); const [effort, setEffort] = useState('xhigh'); const [customEffortOpen, setCustomEffortOpen] = useState(false); const [alias, setAlias] = useState(''); @@ -85,14 +114,20 @@ export function ThinkingAliasesPage() { setLoading(true); setError(''); try { - const [nextEntries, nextSources] = await Promise.all([ + const [nextThinkingEntries, nextThinkingSources, nextSpeedEntries, nextSpeedSources] = await Promise.all([ invoke('get_thinking_aliases'), invoke('get_thinking_alias_sources'), + invoke('get_speed_aliases'), + invoke('get_speed_alias_sources'), ]); - setEntries(nextEntries); - setSources(nextSources); + setThinkingEntries(nextThinkingEntries); + setThinkingSources(nextThinkingSources); + setSpeedEntries(nextSpeedEntries); + setSpeedSources(nextSpeedSources); setSelectedSourceId((current) => ( - nextSources.some((source) => source.id === current) ? current : '' + [...nextThinkingSources, ...nextSpeedSources].some((source) => source.id === current) + ? current + : '' )); } catch (requestError) { setError(String(requestError)); @@ -117,6 +152,18 @@ export function ThinkingAliasesPage() { return () => document.removeEventListener('pointerdown', closeOnOutsideClick); }, [modelPickerOpen]); + const sources = aliasMode === 'fast' ? speedSources : thinkingSources; + const entries = useMemo( + () => combineModelAliasEntries(thinkingEntries, speedEntries), + [speedEntries, thinkingEntries], + ); + + useEffect(() => { + setSelectedSourceId((current) => ( + sources.some((source) => source.id === current) ? current : '' + )); + }, [sources]); + const selectedSource = useMemo( () => sources.find((source) => source.id === selectedSourceId) ?? null, [selectedSourceId, sources], @@ -186,7 +233,9 @@ export function ThinkingAliasesPage() { const normalizedEffort = effort.trim().toLowerCase(); const customEffortSelected = Boolean( - normalizedEffort && !effortOptions.some((option) => option.value === normalizedEffort), + aliasMode === 'reasoning' + && normalizedEffort + && !effortOptions.some((option) => option.value === normalizedEffort), ); const createAlias = async () => { @@ -199,7 +248,7 @@ export function ThinkingAliasesPage() { setError(t('aliases.error.emptyAlias')); return; } - if (!normalizedEffort) { + if (aliasMode === 'reasoning' && !normalizedEffort) { setError(t('aliases.error.emptyEffort')); return; } @@ -208,13 +257,22 @@ export function ThinkingAliasesPage() { setError(''); setNotice(''); try { - const nextEntries = await invoke('create_thinking_alias', { - sourceId: selectedSource.id, - alias: normalizedAlias, - effort: normalizedEffort, - }); - setEntries(nextEntries); - setNotice(t('aliases.created', { alias: normalizedAlias, effort: normalizedEffort })); + 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', { + sourceId: selectedSource.id, + alias: normalizedAlias, + effort: normalizedEffort, + }); + setThinkingEntries(nextEntries); + setNotice(t('aliases.created', { alias: normalizedAlias, effort: normalizedEffort })); + } setAlias(''); } catch (requestError) { setError(String(requestError)); @@ -224,20 +282,30 @@ export function ThinkingAliasesPage() { } }; - const deleteAlias = async (entry: ThinkingAliasEntry) => { + const deleteAlias = async (entry: AliasListEntry) => { if (!window.confirm( - t('aliases.deleteConfirm', { alias: entry.alias }), + t(entry.mode === 'fast' ? 'speedAliases.deleteConfirm' : 'aliases.deleteConfirm', { + alias: entry.alias, + }), )) return; setBusyAlias(entry.alias); setBusyAction('delete'); setError(''); setNotice(''); try { - const nextEntries = await invoke('delete_thinking_alias', { - alias: entry.alias, - }); - setEntries(nextEntries); - setNotice(t('aliases.deleted', { alias: entry.alias })); + if (entry.mode === 'fast') { + const nextEntries = await invoke('delete_speed_alias', { + alias: entry.alias, + }); + setSpeedEntries(nextEntries); + setNotice(t('speedAliases.deleted', { alias: entry.alias })); + } else { + const nextEntries = await invoke('delete_thinking_alias', { + alias: entry.alias, + }); + setThinkingEntries(nextEntries); + setNotice(t('aliases.deleted', { alias: entry.alias })); + } } catch (requestError) { setError(String(requestError)); } finally { @@ -366,9 +434,10 @@ export function ThinkingAliasesPage() { {effortOptions.map((option) => ( ))} + @@ -487,15 +578,19 @@ export function ThinkingAliasesPage() { {entry.alias} - - {entry.effort ?? t('aliases.unboundEffort')} + + {entry.mode === 'fast' + ? t('speedAliases.fast.title') + : entry.effort ?? t('aliases.unboundEffort')}