feat(aliases): integrate Fast options into model alias management
This commit is contained in:
1 parent
d59cee19fa
commit
aa8a7cbd84
7 files changed
+203
-505
No files matched your search
+8
-16
@@ -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': '優先充填',
|
||||
|
||||
+16
-32
@@ -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<MessageKey, string> = {
|
||||
'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<MessageKey, string> = {
|
||||
'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<MessageKey, string> = {
|
||||
'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<MessageKey, string> = {
|
||||
'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',
|
||||
|
||||
@@ -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')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
id="config-subpage-tab-speed-aliases"
|
||||
role="tab"
|
||||
className={activeSubpage === 'speedAliases' ? 'active' : ''}
|
||||
aria-selected={activeSubpage === 'speedAliases'}
|
||||
aria-controls="config-subpage-panel-speed-aliases"
|
||||
tabIndex={activeSubpage === 'speedAliases' ? 0 : -1}
|
||||
onClick={() => setActiveSubpage('speedAliases')}
|
||||
>
|
||||
{t('app.nav.speedAliases')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{activeSubpage === 'general' ? (
|
||||
@@ -888,7 +875,7 @@ export function ConfigPanelPage() {
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
) : activeSubpage === 'aliases' ? (
|
||||
) : (
|
||||
<div
|
||||
className="config-subpage-panel"
|
||||
id="config-subpage-panel-aliases"
|
||||
@@ -897,15 +884,6 @@ export function ConfigPanelPage() {
|
||||
>
|
||||
<ThinkingAliasesPage />
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="config-subpage-panel"
|
||||
id="config-subpage-panel-speed-aliases"
|
||||
role="tabpanel"
|
||||
aria-labelledby="config-subpage-tab-speed-aliases"
|
||||
>
|
||||
<SpeedAliasesPage />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{addDialogOpen ? (
|
||||
|
||||
@@ -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<SpeedAliasEntry[]>([]);
|
||||
const [sources, setSources] = useState<SpeedAliasSource[]>([]);
|
||||
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<HTMLDivElement>(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<SpeedAliasEntry[]>('get_speed_aliases'),
|
||||
invoke<SpeedAliasSource[]>('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<HTMLInputElement>) => {
|
||||
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<SpeedAliasEntry[]>('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<SpeedAliasEntry[]>('delete_speed_alias', {
|
||||
alias: entry.alias,
|
||||
});
|
||||
setEntries(nextEntries);
|
||||
setNotice(t('speedAliases.deleted', { alias: entry.alias }));
|
||||
} catch (requestError) {
|
||||
setError(String(requestError));
|
||||
} finally {
|
||||
setBusyAlias('');
|
||||
setBusyAction('');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="page management-page thinking-alias-page">
|
||||
<div className="thinking-alias-feedback" aria-live="polite">
|
||||
{error ? <div className="management-alert error">{error}</div> : null}
|
||||
{!error && notice ? <div className="management-alert success">{notice}</div> : null}
|
||||
</div>
|
||||
|
||||
<div className="thinking-alias-workbench">
|
||||
<section className="panel thinking-alias-editor-panel">
|
||||
<div className="thinking-alias-panel-heading">
|
||||
<span><Zap size={18} /></span>
|
||||
<div>
|
||||
<h2>{t('speedAliases.create.title')}</h2>
|
||||
<p>{t('speedAliases.create.description')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="thinking-alias-field thinking-model-field">
|
||||
<label htmlFor="speed-model-search">{t('aliases.originalModel')}</label>
|
||||
<div className="thinking-model-picker" ref={modelPickerRef}>
|
||||
<div className="thinking-model-search">
|
||||
{loading ? <LoaderCircle size={15} className="spin" /> : <Search size={15} />}
|
||||
<input
|
||||
id="speed-model-search"
|
||||
role="combobox"
|
||||
aria-autocomplete="list"
|
||||
aria-expanded={modelPickerOpen}
|
||||
aria-controls="speed-model-options"
|
||||
aria-activedescendant={modelPickerOpen && filteredSources[activeSourceIndex]
|
||||
? `speed-model-option-${activeSourceIndex}`
|
||||
: undefined}
|
||||
value={modelPickerOpen ? search : selectedSource?.model ?? ''}
|
||||
onFocus={(event) => {
|
||||
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 ? (
|
||||
<span className="thinking-source-kind">
|
||||
{thinkingAliasSourceKindLabel(selectedSource.kind)}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
{modelPickerOpen ? (
|
||||
<div
|
||||
className="thinking-model-list"
|
||||
id="speed-model-options"
|
||||
role="listbox"
|
||||
aria-label={t('aliases.availableModels')}
|
||||
>
|
||||
{filteredSources.length === 0 ? (
|
||||
<div className="thinking-model-empty">
|
||||
{sources.length ? t('aliases.noMatch') : t('aliases.noModels')}
|
||||
</div>
|
||||
) : filteredSources.map((source, index) => {
|
||||
const selected = source.id === selectedSourceId;
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
role="option"
|
||||
aria-selected={selected}
|
||||
id={`speed-model-option-${index}`}
|
||||
className={`${selected ? 'selected ' : ''}${index === activeSourceIndex ? 'active' : ''}`.trim()}
|
||||
key={source.id}
|
||||
onMouseEnter={() => setActiveSourceIndex(index)}
|
||||
onClick={() => chooseSource(source)}
|
||||
disabled={Boolean(busyAlias)}
|
||||
>
|
||||
<span className="thinking-model-option-copy">
|
||||
<span>
|
||||
<strong title={source.model}>{source.model}</strong>
|
||||
{source.displayName && source.displayName !== source.model
|
||||
? <small>{source.displayName}</small>
|
||||
: null}
|
||||
</span>
|
||||
<span className="thinking-model-source">
|
||||
<em>{thinkingAliasSourceKindLabel(source.kind)}</em>
|
||||
<small title={source.provider}>{source.provider}</small>
|
||||
</span>
|
||||
</span>
|
||||
{selected ? <Check size={15} /> : null}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{selectedSource ? (
|
||||
<div className="thinking-model-selection">
|
||||
<span>{thinkingAliasSourceKindLabel(selectedSource.kind)}</span>
|
||||
<strong title={selectedSource.provider}>{selectedSource.provider}</strong>
|
||||
</div>
|
||||
) : (
|
||||
<small className="thinking-model-hint">{t('aliases.sourceHint')}</small>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="thinking-alias-preview">
|
||||
<Zap size={18} />
|
||||
<div>
|
||||
<span>{t('speedAliases.fast.title')}</span>
|
||||
<code>service_tier = priority</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="thinking-alias-section-divider" aria-hidden="true" />
|
||||
|
||||
<div className="thinking-alias-field">
|
||||
<div className="thinking-field-heading">
|
||||
<strong>{t('aliases.aliasName.title')}</strong>
|
||||
<span>{t('aliases.aliasName.description')}</span>
|
||||
</div>
|
||||
<input
|
||||
id="speed-alias-name"
|
||||
className="thinking-alias-input"
|
||||
value={alias}
|
||||
onChange={(event) => setAlias(event.currentTarget.value)}
|
||||
placeholder={selectedSource
|
||||
? t('speedAliases.aliasName.example', { model: selectedSource.model })
|
||||
: t('aliases.aliasName.selectFirst')}
|
||||
disabled={Boolean(busyAlias)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="thinking-alias-preview">
|
||||
<GitFork size={18} />
|
||||
<div>
|
||||
<span>{selectedSource?.model || t('aliases.notSelected')} <ArrowRight size={13} /> {alias || t('aliases.enterAlias')}</span>
|
||||
<code>service_tier = priority</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="primary-button thinking-alias-create"
|
||||
onClick={() => void createAlias()}
|
||||
disabled={loading || Boolean(busyAlias) || !selectedSource || !alias.trim()}
|
||||
>
|
||||
{busyAction === 'create' ? <LoaderCircle size={16} className="spin" /> : <Zap size={16} />}
|
||||
{busyAction === 'create' ? t('speedAliases.creating') : t('speedAliases.create')}
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<section className="panel thinking-alias-list-panel">
|
||||
<div className="thinking-alias-list-heading">
|
||||
<div>
|
||||
<h2>{t('speedAliases.createdList.title')}</h2>
|
||||
<span>{t('speedAliases.createdList.description')}</span>
|
||||
</div>
|
||||
<strong>{entries.length}</strong>
|
||||
</div>
|
||||
|
||||
<div className="thinking-alias-list">
|
||||
{loading ? (
|
||||
<div className="management-loading"><LoaderCircle size={20} className="spin" />{t('aliases.loadingConfig')}</div>
|
||||
) : entries.length === 0 ? (
|
||||
<div className="management-empty">
|
||||
<Zap size={25} />
|
||||
<strong>{t('speedAliases.empty.title')}</strong>
|
||||
<span>{t('speedAliases.empty.description')}</span>
|
||||
</div>
|
||||
) : entries.map((entry) => (
|
||||
<article className="thinking-alias-row" key={`${entry.kind}:${entry.provider}:${entry.alias}`}>
|
||||
<div className="thinking-alias-route">
|
||||
<div className="thinking-alias-route-source">
|
||||
<span title={entry.sourceModel}>{entry.sourceModel}</span>
|
||||
<small>
|
||||
<em>{thinkingAliasSourceKindLabel(entry.kind)}</em>
|
||||
<span title={entry.provider}>{entry.provider}</span>
|
||||
</small>
|
||||
</div>
|
||||
<ArrowRight size={14} />
|
||||
<strong title={entry.alias}>{entry.alias}</strong>
|
||||
</div>
|
||||
<span className="thinking-effort-badge">{entry.serviceTier}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button danger"
|
||||
onClick={() => void deleteAlias(entry)}
|
||||
disabled={Boolean(busyAlias)}
|
||||
title={t('speedAliases.delete', { alias: entry.alias })}
|
||||
>
|
||||
{busyAction === 'delete' && busyAlias === entry.alias
|
||||
? <LoaderCircle size={15} className="spin" />
|
||||
: <Trash2 size={15} />}
|
||||
</button>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -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<ThinkingAliasEntry[]>([]);
|
||||
const [sources, setSources] = useState<ThinkingAliasSource[]>([]);
|
||||
const [thinkingEntries, setThinkingEntries] = useState<ThinkingAliasEntry[]>([]);
|
||||
const [speedEntries, setSpeedEntries] = useState<SpeedAliasEntry[]>([]);
|
||||
const [thinkingSources, setThinkingSources] = useState<ThinkingAliasSource[]>([]);
|
||||
const [speedSources, setSpeedSources] = useState<ThinkingAliasSource[]>([]);
|
||||
const [selectedSourceId, setSelectedSourceId] = useState('');
|
||||
const [aliasMode, setAliasMode] = useState<AliasMode>('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<ThinkingAliasEntry[]>('get_thinking_aliases'),
|
||||
invoke<ThinkingAliasSource[]>('get_thinking_alias_sources'),
|
||||
invoke<SpeedAliasEntry[]>('get_speed_aliases'),
|
||||
invoke<ThinkingAliasSource[]>('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<ThinkingAliasEntry[]>('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<SpeedAliasEntry[]>('create_speed_alias', {
|
||||
sourceId: selectedSource.id,
|
||||
alias: normalizedAlias,
|
||||
});
|
||||
setSpeedEntries(nextEntries);
|
||||
setNotice(t('speedAliases.created', { alias: normalizedAlias }));
|
||||
} else {
|
||||
const nextEntries = await invoke<ThinkingAliasEntry[]>('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<ThinkingAliasEntry[]>('delete_thinking_alias', {
|
||||
alias: entry.alias,
|
||||
});
|
||||
setEntries(nextEntries);
|
||||
setNotice(t('aliases.deleted', { alias: entry.alias }));
|
||||
if (entry.mode === 'fast') {
|
||||
const nextEntries = await invoke<SpeedAliasEntry[]>('delete_speed_alias', {
|
||||
alias: entry.alias,
|
||||
});
|
||||
setSpeedEntries(nextEntries);
|
||||
setNotice(t('speedAliases.deleted', { alias: entry.alias }));
|
||||
} else {
|
||||
const nextEntries = await invoke<ThinkingAliasEntry[]>('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) => (
|
||||
<button
|
||||
type="button"
|
||||
className={effort.trim().toLowerCase() === option.value ? 'active' : ''}
|
||||
className={aliasMode === 'reasoning' && effort.trim().toLowerCase() === option.value ? 'active' : ''}
|
||||
key={option.value}
|
||||
onClick={() => {
|
||||
setAliasMode('reasoning');
|
||||
chooseEffort(option.value);
|
||||
setCustomEffortOpen(false);
|
||||
}}
|
||||
@@ -378,10 +447,23 @@ export function ThinkingAliasesPage() {
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
className={aliasMode === 'fast' ? 'active fast' : 'fast'}
|
||||
onClick={() => {
|
||||
setAliasMode('fast');
|
||||
setCustomEffortOpen(false);
|
||||
}}
|
||||
disabled={Boolean(busyAlias)}
|
||||
title={t('aliases.fast.description')}
|
||||
>
|
||||
<Zap size={13} /> Fast
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={customEffortSelected ? 'active custom' : 'custom'}
|
||||
onClick={() => {
|
||||
setAliasMode('reasoning');
|
||||
if (!customEffortSelected) chooseEffort('');
|
||||
setCustomEffortOpen(true);
|
||||
}}
|
||||
@@ -397,7 +479,10 @@ export function ThinkingAliasesPage() {
|
||||
<input
|
||||
id="thinking-effort-custom"
|
||||
value={customEffortSelected ? effort : ''}
|
||||
onChange={(event) => chooseEffort(event.currentTarget.value)}
|
||||
onChange={(event) => {
|
||||
setAliasMode('reasoning');
|
||||
chooseEffort(event.currentTarget.value);
|
||||
}}
|
||||
placeholder={t('aliases.customPlaceholder')}
|
||||
maxLength={64}
|
||||
spellCheck={false}
|
||||
@@ -429,17 +514,21 @@ export function ThinkingAliasesPage() {
|
||||
value={alias}
|
||||
onChange={(event) => setAlias(event.currentTarget.value)}
|
||||
placeholder={selectedSource
|
||||
? t('aliases.aliasName.example', { model: selectedSource.model, effort: normalizedEffort || 'high' })
|
||||
? aliasMode === 'fast'
|
||||
? t('speedAliases.aliasName.example', { model: selectedSource.model })
|
||||
: t('aliases.aliasName.example', { model: selectedSource.model, effort: normalizedEffort || 'high' })
|
||||
: t('aliases.aliasName.selectFirst')}
|
||||
disabled={Boolean(busyAlias)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="thinking-alias-preview">
|
||||
<BrainCircuit size={18} />
|
||||
{aliasMode === 'fast' ? <Zap size={18} /> : <BrainCircuit size={18} />}
|
||||
<div>
|
||||
<span>{selectedSource?.model || t('aliases.notSelected')} <ArrowRight size={13} /> {alias || t('aliases.enterAlias')}</span>
|
||||
<code>{selectedSource?.protocol === 'openai' ? 'reasoning_effort' : 'reasoning.effort'} = {normalizedEffort || t('aliases.notSet')}</code>
|
||||
<code>{aliasMode === 'fast'
|
||||
? 'service_tier = priority'
|
||||
: `${selectedSource?.protocol === 'openai' ? 'reasoning_effort' : 'reasoning.effort'} = ${normalizedEffort || t('aliases.notSet')}`}</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -447,9 +536,11 @@ export function ThinkingAliasesPage() {
|
||||
type="button"
|
||||
className="primary-button thinking-alias-create"
|
||||
onClick={() => void createAlias()}
|
||||
disabled={loading || Boolean(busyAlias) || !selectedSource || !effort.trim() || !alias.trim()}
|
||||
disabled={loading || Boolean(busyAlias) || !selectedSource || (aliasMode === 'reasoning' && !effort.trim()) || !alias.trim()}
|
||||
>
|
||||
{busyAction === 'create' ? <LoaderCircle size={16} className="spin" /> : <GitFork size={16} />}
|
||||
{busyAction === 'create'
|
||||
? <LoaderCircle size={16} className="spin" />
|
||||
: aliasMode === 'fast' ? <Zap size={16} /> : <GitFork size={16} />}
|
||||
{busyAction === 'create' ? t('aliases.creating') : t('aliases.create')}
|
||||
</button>
|
||||
</section>
|
||||
@@ -487,15 +578,19 @@ export function ThinkingAliasesPage() {
|
||||
<ArrowRight size={14} />
|
||||
<strong title={entry.alias}>{entry.alias}</strong>
|
||||
</div>
|
||||
<span className={`thinking-effort-badge ${entry.effort ? '' : 'missing'}`}>
|
||||
{entry.effort ?? t('aliases.unboundEffort')}
|
||||
<span className={`thinking-effort-badge ${entry.mode === 'fast' ? 'fast' : entry.effort ? '' : 'missing'}`}>
|
||||
{entry.mode === 'fast'
|
||||
? t('speedAliases.fast.title')
|
||||
: entry.effort ?? t('aliases.unboundEffort')}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button danger"
|
||||
onClick={() => void deleteAlias(entry)}
|
||||
disabled={Boolean(busyAlias)}
|
||||
title={t('aliases.delete', { alias: entry.alias })}
|
||||
title={t(entry.mode === 'fast' ? 'speedAliases.delete' : 'aliases.delete', {
|
||||
alias: entry.alias,
|
||||
})}
|
||||
>
|
||||
{busyAction === 'delete' && busyAlias === entry.alias
|
||||
? <LoaderCircle size={15} className="spin" />
|
||||
|
||||
+20
-1
@@ -4019,7 +4019,7 @@ textarea:focus-visible {
|
||||
|
||||
.thinking-effort-options {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(7, minmax(0, 1fr));
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
@@ -4051,6 +4051,19 @@ textarea:focus-visible {
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
.thinking-effort-options button.fast {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.thinking-effort-options button.fast.active {
|
||||
border-color: var(--theme-d7a447);
|
||||
background: var(--theme-fff8eb);
|
||||
color: var(--theme-805a24);
|
||||
}
|
||||
|
||||
.thinking-effort-custom {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 30px;
|
||||
@@ -4277,6 +4290,12 @@ textarea:focus-visible {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.thinking-effort-badge.fast {
|
||||
border-color: var(--theme-d7a447);
|
||||
background: var(--theme-fff8eb);
|
||||
color: var(--theme-805a24);
|
||||
}
|
||||
|
||||
@media (min-width: 1081px) {
|
||||
.thinking-alias-list-panel .thinking-alias-row {
|
||||
grid-template-columns: minmax(0, 1fr) auto 34px;
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import { thinkingAliasSourceKindLabel } from '../src/pages/ThinkingAliasesPage';
|
||||
import {
|
||||
combineModelAliasEntries,
|
||||
thinkingAliasSourceKindLabel,
|
||||
} from '../src/pages/ThinkingAliasesPage';
|
||||
|
||||
describe('思考别名', () => {
|
||||
test('区分同名模型的接入来源', () => {
|
||||
@@ -9,3 +12,29 @@ describe('思考别名', () => {
|
||||
expect(thinkingAliasSourceKindLabel('custom')).toBe('其他来源');
|
||||
});
|
||||
});
|
||||
|
||||
describe('统一模型别名列表', () => {
|
||||
test('同时显示思考别名和 Fast 速度别名', () => {
|
||||
const entries = combineModelAliasEntries(
|
||||
[{
|
||||
sourceModel: 'gpt-thinking',
|
||||
alias: 'gpt-high',
|
||||
effort: 'high',
|
||||
provider: 'Provider B',
|
||||
kind: 'openai-compatible',
|
||||
}],
|
||||
[{
|
||||
sourceModel: 'gpt-fast-source',
|
||||
alias: 'gpt-fast',
|
||||
serviceTier: 'priority',
|
||||
provider: 'Provider A',
|
||||
kind: 'codex-api',
|
||||
}],
|
||||
);
|
||||
|
||||
expect(entries.map((entry) => [entry.alias, entry.mode])).toEqual([
|
||||
['gpt-fast', 'fast'],
|
||||
['gpt-high', 'reasoning'],
|
||||
]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user