feat(model-aliases): restore Fast service-tier option

- enable Fast for Codex API, OpenAI-compatible, and Codex OAuth sources
- apply service_tier=priority as an independent payload override
- support Fast-only and reasoning-effort combinations
- disable Fast until a supported model source is selected
This commit is contained in:
lzt404 committed 2026-09-03 02:44:48 +08:00
1 parent 708a834e24
commit cff6a21974
9 files changed
+443 -112

No files matched your search

+12 -3
View File
@@ -320,12 +320,14 @@ export const en: Record<MessageKey, string> = {
'aliases.error.emptyAlias': 'The model alias cannot be empty',
'aliases.error.emptyEffort': 'The reasoning effort cannot be empty',
'aliases.error.unsupportedEffort': 'This model source has no available reasoning-effort levels',
'aliases.error.unsupportedFast': 'Fast is only available for Codex API, OpenAI-compatible, and Codex OAuth model sources',
'aliases.created': 'Created {alias} with reasoning effort fixed to {effort}',
'aliases.createdCombined': 'Created {alias} with reasoning effort {effort} and Fast enabled',
'aliases.createdPlain': 'Created model alias {alias}',
'aliases.deleteConfirm': 'Delete the model alias “{alias}”?\nIts reasoning-effort override will also be removed.',
'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': 'Create a separate model entry and fix its reasoning effort for the selected source',
'aliases.create.description': 'Create a separate model entry and override its reasoning effort and Fast request tier',
'aliases.originalModel': 'Original Model',
'aliases.loadingModels': 'Loading models',
'aliases.searchModel': 'Search and select an original model',
@@ -339,6 +341,10 @@ export const en: Record<MessageKey, string> = {
'aliases.effort.none': 'Not set',
'aliases.effort.noneHint': 'Do not override reasoning effort',
'aliases.effort.unsupported': 'This model source does not report any available reasoning-effort levels.',
'aliases.fast.title': 'Fast Acceleration',
'aliases.fast.enabled': 'Enabled; requests use the priority service tier',
'aliases.fast.disabled': 'Disabled; preserve the client service_tier',
'aliases.fast.unsupported': 'Fast is only available for Codex API, OpenAI-compatible, and Codex OAuth model sources.',
'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',
@@ -350,12 +356,15 @@ export const en: Record<MessageKey, string> = {
'aliases.creating': 'Creating',
'aliases.create': 'Create Alias',
'aliases.createdList.title': 'Created Aliases',
'aliases.createdList.description': 'Each alias shows its fixed reasoning effort',
'aliases.createdList.description': 'Each alias shows its fixed reasoning effort and Fast status',
'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 request tier fixed',
'speedAliases.fast.title': 'Fast',
'speedAliases.aliasName.example': 'For example: {model}-fast',
'config.routing.roundRobin': 'Round Robin',
'config.routing.fillFirst': 'Fill First',
'config.tabs.label': 'Advanced settings sections',
+12 -3
View File
@@ -318,12 +318,14 @@ export const zhCN = {
'aliases.error.emptyAlias': '别名模型不能为空',
'aliases.error.emptyEffort': '思考强度不能为空',
'aliases.error.unsupportedEffort': '该模型来源没有可用的思考强度等级',
'aliases.error.unsupportedFast': 'Fast 仅支持 Codex API、OpenAI 兼容和 Codex OAuth 模型源',
'aliases.created': '已创建 {alias},思考强度固定为 {effort}',
'aliases.createdCombined': '已创建 {alias},思考强度固定为 {effort},同时启用 Fast',
'aliases.createdPlain': '已创建别名 {alias}',
'aliases.deleteConfirm': '确定删除别名模型“{alias}”吗?\n对应的思考强度覆写规则也会被移除。',
'aliases.deleteConfirm': '确定删除别名模型“{alias}”吗?\n对应的思考强度和 Fast 覆写规则也会被移除。',
'aliases.deleted': '已删除 {alias}',
'aliases.create.title': '创建模型别名',
'aliases.create.description': '为模型创建独立入口,并按模型来源固定思考强度',
'aliases.create.description': '为模型创建独立入口,并按模型来源覆写思考强度和 Fast 请求层级',
'aliases.originalModel': '原模型',
'aliases.loadingModels': '正在读取模型',
'aliases.searchModel': '搜索并选择原模型',
@@ -337,6 +339,10 @@ export const zhCN = {
'aliases.effort.none': '不设置',
'aliases.effort.noneHint': '不覆盖思考强度',
'aliases.effort.unsupported': '这个模型来源没有报告可用的思考强度等级。',
'aliases.fast.title': 'Fast 加速',
'aliases.fast.enabled': '已启用,固定使用 priority 请求层级',
'aliases.fast.disabled': '未启用,保留客户端原始 service_tier',
'aliases.fast.unsupported': '仅 Codex API、OpenAI 兼容和 Codex OAuth 模型源支持 Fast。',
'aliases.aliasName.title': '别名模型名称',
'aliases.aliasName.description': '由你填写,仅新增入口,不会改名原模型',
'aliases.aliasName.autoDescription': '根据原模型和已选选项自动生成,可按需修改;不会改名原模型',
@@ -348,12 +354,15 @@ 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.unboundEffort': '未绑定强度',
'aliases.delete': '删除 {alias}',
'speedAliases.created': '已创建 {alias},并固定使用 Fast 请求层级',
'speedAliases.fast.title': 'Fast',
'speedAliases.aliasName.example': '例如 {model}-fast',
'config.routing.roundRobin': '轮询',
'config.routing.fillFirst': '优先填充',
'config.tabs.label': '高级设置子页面',
+160 -29
View File
@@ -15,6 +15,7 @@ import {
LoaderCircle,
Search,
Trash2,
Zap,
} from 'lucide-react';
import { getCurrentLocale, translate, useI18n } from '../i18n';
@@ -29,6 +30,19 @@ 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;
@@ -41,6 +55,7 @@ type ThinkingAliasSource = {
type ModelAliasSource = ThinkingAliasSource & {
supportsReasoning: boolean;
supportsFast: boolean;
};
const effortOptions = [
@@ -51,16 +66,49 @@ 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<string, AliasListEntry>();
const entryKey = (
entry: Pick<ThinkingAliasEntry, 'kind' | 'provider' | 'sourceModel' | 'alias' | 'oauthChannel'>,
) => (
[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 });
});
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<string, ModelAliasSource>();
[...baseSources, ...thinkingSources].forEach((source) => {
[...baseSources, ...speedSources, ...thinkingSources].forEach((source) => {
sources.set(source.id, {
...source,
supportsReasoning: reasoningSourceIds.has(source.id),
supportsFast: speedSourceIds.has(source.id),
});
});
return [...sources.values()];
@@ -69,12 +117,13 @@ 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) return `${normalizedModel}-alias`;
return `${normalizedModel}-${normalizedEffort}`;
if (!normalizedEffort && !fast) return `${normalizedModel}-alias`;
return `${normalizedModel}${normalizedEffort ? `-${normalizedEffort}` : ''}${fast ? '-fast' : ''}`;
};
export const uniqueModelAlias = (
@@ -127,10 +176,13 @@ const thinkingAliasSourceDetail = (source: ThinkingAliasSource) => (
export function ThinkingAliasesPage() {
const { t } = useI18n();
const [thinkingEntries, setThinkingEntries] = useState<ThinkingAliasEntry[]>([]);
const [speedEntries, setSpeedEntries] = useState<SpeedAliasEntry[]>([]);
const [baseSources, setBaseSources] = useState<ThinkingAliasSource[]>([]);
const [thinkingSources, setThinkingSources] = useState<ThinkingAliasSource[]>([]);
const [speedSources, setSpeedSources] = useState<ThinkingAliasSource[]>([]);
const [selectedSourceId, setSelectedSourceId] = useState('');
const [effort, setEffort] = useState('');
const [fastEnabled, setFastEnabled] = useState(false);
const [alias, setAlias] = useState('');
const [search, setSearch] = useState('');
const [modelPickerOpen, setModelPickerOpen] = useState(false);
@@ -147,14 +199,24 @@ export function ThinkingAliasesPage() {
setLoading(true);
setError('');
try {
const [nextThinkingEntries, nextBaseSources, nextThinkingSources] = await Promise.all([
const [
nextThinkingEntries,
nextBaseSources,
nextThinkingSources,
nextSpeedEntries,
nextSpeedSources,
] = await Promise.all([
invoke<ThinkingAliasEntry[]>('get_thinking_aliases'),
invoke<ThinkingAliasSource[]>('get_model_alias_sources'),
invoke<ThinkingAliasSource[]>('get_thinking_alias_sources'),
invoke<SpeedAliasEntry[]>('get_speed_aliases'),
invoke<ThinkingAliasSource[]>('get_speed_alias_sources'),
]);
setThinkingEntries(nextThinkingEntries);
setSpeedEntries(nextSpeedEntries);
setBaseSources(nextBaseSources);
setThinkingSources(nextThinkingSources);
setSpeedSources(nextSpeedSources);
setSelectedSourceId((current) => (
nextBaseSources.some((source) => source.id === current)
? current
@@ -184,8 +246,12 @@ export function ThinkingAliasesPage() {
}, [modelPickerOpen]);
const sources = useMemo(
() => combineModelAliasSources(baseSources, thinkingSources),
[baseSources, thinkingSources],
() => combineModelAliasSources(baseSources, thinkingSources, speedSources),
[baseSources, speedSources, thinkingSources],
);
const entries = useMemo(
() => combineModelAliasEntries(thinkingEntries, speedEntries),
[speedEntries, thinkingEntries],
);
useEffect(() => {
@@ -198,6 +264,13 @@ export function ThinkingAliasesPage() {
() => sources.find((source) => source.id === selectedSourceId) ?? null,
[selectedSourceId, sources],
);
const fastAvailable = Boolean(selectedSource?.supportsFast);
useEffect(() => {
if (!fastAvailable) {
setFastEnabled(false);
}
}, [fastAvailable]);
const filteredSources = useMemo(() => {
const query = search.trim().toLowerCase();
@@ -228,6 +301,7 @@ export function ThinkingAliasesPage() {
const chooseSource = (source: ModelAliasSource) => {
setSelectedSourceId(source.id);
setEffort('');
setFastEnabled((current) => current && source.supportsFast);
setAlias('');
generatedAliasRef.current = '';
setModelPickerOpen(false);
@@ -265,7 +339,7 @@ export function ThinkingAliasesPage() {
};
const normalizedEffort = effort.trim().toLowerCase();
const defaultAlias = defaultModelAlias(selectedSource?.model, normalizedEffort);
const defaultAlias = defaultModelAlias(selectedSource?.model, normalizedEffort, fastEnabled);
const uniqueDefaultAlias = uniqueModelAlias(defaultAlias, sources.map((source) => source.model));
const availableEfforts = selectedSource?.reasoningLevels ?? [];
const visibleEffortOptions = availableEfforts.map((value) => {
@@ -295,6 +369,10 @@ 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;
@@ -304,14 +382,32 @@ export function ThinkingAliasesPage() {
setError('');
setNotice('');
try {
await invoke<ThinkingAliasEntry[]>('create_thinking_alias', {
sourceId: selectedSource.id,
alias: normalizedAlias,
effort: normalizedEffort,
});
setNotice(normalizedEffort
? t('aliases.created', { alias: normalizedAlias, effort: normalizedEffort })
: t('aliases.createdPlain', { alias: normalizedAlias }));
if (normalizedEffort) {
await invoke<ThinkingAliasEntry[]>('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<SpeedAliasEntry[]>('create_speed_alias', {
sourceId: selectedSource.id,
alias: normalizedAlias,
});
setNotice(t('speedAliases.created', { alias: normalizedAlias }));
} else {
await invoke<ThinkingAliasEntry[]>('create_thinking_alias', {
sourceId: selectedSource.id,
alias: normalizedAlias,
effort: '',
fast: false,
});
setNotice(t('aliases.createdPlain', { alias: normalizedAlias }));
}
setAlias('');
await load();
} catch (requestError) {
@@ -322,7 +418,7 @@ export function ThinkingAliasesPage() {
}
};
const deleteAlias = async (entry: ThinkingAliasEntry) => {
const deleteAlias = async (entry: AliasListEntry) => {
if (!window.confirm(
t('aliases.deleteConfirm', { alias: entry.alias }),
)) return;
@@ -331,10 +427,17 @@ export function ThinkingAliasesPage() {
setError('');
setNotice('');
try {
await invoke<ThinkingAliasEntry[]>('delete_thinking_alias', {
alias: entry.alias,
oauthChannel: entry.oauthChannel,
});
if (entry.effort || !entry.serviceTier) {
await invoke<ThinkingAliasEntry[]>('delete_thinking_alias', {
alias: entry.alias,
oauthChannel: entry.oauthChannel,
});
} else {
await invoke<SpeedAliasEntry[]>('delete_speed_alias', {
alias: entry.alias,
oauthChannel: entry.oauthChannel,
});
}
setNotice(t('aliases.deleted', { alias: entry.alias }));
await load();
} catch (requestError) {
@@ -392,6 +495,7 @@ export function ThinkingAliasesPage() {
) {
setSelectedSourceId('');
setEffort('');
setFastEnabled(false);
setAlias('');
generatedAliasRef.current = '';
}
@@ -503,6 +607,31 @@ export function ThinkingAliasesPage() {
) : null}
</div>
<div className="thinking-alias-field">
<div className="thinking-field-heading">
<strong>{t('aliases.fast.title')}</strong>
</div>
<label className={`thinking-fast-option${fastEnabled ? ' active' : ''}`}>
<span className="thinking-fast-option-copy">
<span><Zap size={15} /> Fast</span>
<small>{fastEnabled ? t('aliases.fast.enabled') : t('aliases.fast.disabled')}</small>
</span>
<span className="switch-control thinking-fast-switch">
<input
type="checkbox"
checked={fastEnabled}
onChange={(event) => setFastEnabled(event.currentTarget.checked)}
disabled={Boolean(busyAlias) || !fastAvailable}
aria-label={t('aliases.fast.title')}
/>
<span className="switch-track" />
</span>
</label>
{selectedSource && !selectedSource.supportsFast ? (
<small className="thinking-model-hint">{t('aliases.fast.unsupported')}</small>
) : null}
</div>
<div className="thinking-alias-section-divider" aria-hidden="true" />
<div className="thinking-alias-field">
@@ -518,19 +647,18 @@ export function ThinkingAliasesPage() {
placeholder={selectedSource
? normalizedEffort
? t('aliases.aliasName.example', { model: selectedSource.model, effort: normalizedEffort })
: t('aliases.aliasName.example', { model: selectedSource.model, effort: 'alias' })
: fastEnabled
? t('speedAliases.aliasName.example', { model: selectedSource.model })
: t('aliases.aliasName.example', { model: selectedSource.model, effort: 'alias' })
: t('aliases.aliasName.selectFirst')}
disabled={Boolean(busyAlias)}
/>
</div>
<div className="thinking-alias-preview">
<BrainCircuit size={18} />
{fastEnabled && !normalizedEffort ? <Zap size={18} /> : <BrainCircuit size={18} />}
<div>
<span>{selectedSource?.model || t('aliases.notSelected')} <ArrowRight size={13} /> {alias || t('aliases.enterAlias')}</span>
<code>{normalizedEffort
? `${t('aliases.effort.title')} = ${normalizedEffort}`
: t('aliases.notSet')}</code>
</div>
</div>
@@ -542,7 +670,7 @@ export function ThinkingAliasesPage() {
>
{busyAction === 'create'
? <LoaderCircle size={16} className="spin" />
: <GitFork size={16} />}
: fastEnabled && !normalizedEffort ? <Zap size={16} /> : <GitFork size={16} />}
{busyAction === 'create' ? t('aliases.creating') : t('aliases.create')}
</button>
</section>
@@ -553,19 +681,19 @@ export function ThinkingAliasesPage() {
<h2>{t('aliases.createdList.title')}</h2>
<span>{t('aliases.createdList.description')}</span>
</div>
<strong>{thinkingEntries.length}</strong>
<strong>{entries.length}</strong>
</div>
<div className="thinking-alias-list">
{loading ? (
<div className="management-loading"><LoaderCircle size={20} className="spin" />{t('aliases.loadingConfig')}</div>
) : thinkingEntries.length === 0 ? (
) : entries.length === 0 ? (
<div className="management-empty">
<GitFork size={25} />
<strong>{t('aliases.empty.title')}</strong>
<span>{t('aliases.empty.description')}</span>
</div>
) : thinkingEntries.map((entry) => (
) : 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">
@@ -584,6 +712,9 @@ export function ThinkingAliasesPage() {
{entry.effort ? (
<span className="thinking-effort-badge">{entry.effort}</span>
) : null}
{entry.serviceTier ? (
<span className="thinking-effort-badge fast">{t('speedAliases.fast.title')}</span>
) : null}
</div>
<button
type="button"
+70
View File
@@ -4788,6 +4788,70 @@ textarea:focus-visible {
opacity: 0.45;
}
.thinking-fast-option {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
width: 100%;
min-height: 48px;
padding: 8px 10px;
border: 1px solid var(--theme-d5d2cb);
border-radius: 7px;
background: var(--theme-fff);
color: var(--theme-7a746c);
cursor: pointer;
transition: border-color 140ms ease, background 140ms ease, color 140ms ease;
}
.thinking-fast-option:hover {
border-color: var(--theme-d7a447);
}
.thinking-fast-option.active {
border-color: var(--theme-d7a447);
background: var(--theme-fff8eb);
color: var(--theme-805a24);
}
.thinking-fast-option-copy {
display: grid;
min-width: 0;
gap: 2px;
}
.thinking-fast-option-copy > span {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: var(--font-size-section);
font-weight: 800;
}
.thinking-fast-option-copy small {
color: var(--theme-9a938a);
font-size: var(--font-size-label);
}
.thinking-fast-option.active .thinking-fast-option-copy small {
color: var(--theme-805a24);
}
.thinking-fast-option:has(input:disabled) {
cursor: not-allowed;
opacity: 0.55;
}
.thinking-fast-switch {
width: 42px;
height: 23px;
}
.thinking-fast-switch input:checked + .switch-track {
border-color: var(--theme-d7a447);
background: var(--theme-d7a447);
}
.thinking-alias-input {
width: 100%;
height: 36px;
@@ -4985,6 +5049,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;