fix(aliases): allow reasoning effort and Fast mode together

This commit is contained in:
lzt404 committed 2026-08-02 14:57:31 +08:00
1 parent aa8a7cbd84
commit eaefcd6d9b
6 files changed
+307 -105

No files matched your search

+43 -1
View File
@@ -1820,6 +1820,7 @@ async fn create_thinking_alias(
source_id: String,
alias: String,
effort: String,
fast: Option<bool>,
) -> Result<Vec<ThinkingAliasEntry>, 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<String, String> {
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<String, String> {
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<String,
root.remove(yaml_key("oauth-model-alias"));
}
remove_thinking_payload_model(root, alias)?;
remove_speed_payload_model(root, alias)?;
render_updated_core_yaml(&mut document, updated)
}
@@ -18818,6 +18837,29 @@ model:
);
}
#[test]
fn model_alias_can_apply_reasoning_and_fast_together() {
let source = test_codex_oauth_thinking_source("gpt-5.6-sol");
let rendered =
add_model_alias_to_yaml("{}\n", &source, "gpt-5.6-sol-xhigh-fast", "xhigh", true)
.unwrap();
assert!(rendered.contains("reasoning.effort: xhigh"), "{rendered}");
assert!(rendered.contains("service_tier: priority"), "{rendered}");
assert_eq!(thinking_aliases_from_yaml(&rendered).unwrap().len(), 1);
assert_eq!(speed_aliases_from_yaml(&rendered).unwrap().len(), 1);
let restored =
remove_thinking_alias_from_yaml(&rendered, "gpt-5.6-sol-xhigh-fast").unwrap();
assert!(!restored.contains("gpt-5.6-sol-xhigh-fast"), "{restored}");
assert!(!restored.contains("service_tier: priority"), "{restored}");
let legacy = "oauth-model-alias:\n codex:\n - name: gpt-5.6-sol\n alias: legacy-combined\n fork: true\npayload:\n override:\n - models:\n - name: legacy-combined\n protocol: codex\n params:\n reasoning.effort: high\n - models:\n - name: legacy-combined\n protocol: codex\n params:\n service_tier: priority\n";
let restored = remove_thinking_alias_from_yaml(legacy, "legacy-combined").unwrap();
assert!(!restored.contains("legacy-combined"), "{restored}");
assert!(!restored.contains("service_tier: priority"), "{restored}");
}
#[test]
fn speed_alias_adds_fast_service_tier_and_removes_only_its_rule() {
let input = "payload:\n override:\n - models:\n - name: existing-thinker\n protocol: codex\n params:\n reasoning.effort: xhigh\n";
+14 -5
View File
@@ -203,11 +203,14 @@ export const jaOverrides = {
'aliases.error.selectModel': '先に元のモデルを選択してください',
'aliases.error.emptyAlias': 'エイリアスモデル名を入力してください',
'aliases.error.emptyEffort': '推論レベルを入力してください',
'aliases.error.emptyOptions': '推論レベルを設定するか、Fast を有効にしてください',
'aliases.error.unsupportedEffort': 'このモデルでは推論レベルを設定できないため、Fast オプションのみ使用できます',
'aliases.created': '{alias} を作成し、推論レベルを {effort} に固定しました',
'aliases.deleteConfirm': 'エイリアスモデル「{alias}」を削除しますか?\n対応する推論レベルの上書きルールも削除されます。',
'aliases.createdCombined': '{alias} を作成し、推論レベル {effort} と Fast を有効にしました',
'aliases.deleteConfirm': 'エイリアスモデル「{alias}」を削除しますか?\n対応する推論レベルと Fast の上書きルールも削除されます。',
'aliases.deleted': '{alias} を削除しました',
'aliases.create.title': 'モデルエイリアスを作成',
'aliases.create.description': '推論レベルと Fast 速度階層を一か所で設定します',
'aliases.create.description': '推論レベルと Fast 速度階層は個別にも同時にも設定できます',
'aliases.originalModel': '元のモデル',
'aliases.loadingModels': 'モデルを読み込んでいます',
'aliases.searchModel': '元のモデルを検索して選択',
@@ -216,9 +219,15 @@ export const jaOverrides = {
'aliases.noModels': '選択可能なモデルがありません',
'aliases.sourceLabel': 'ソース:{source}',
'aliases.sourceHint': '同名モデルは Codex OAuth、Codex API、OpenAI 互換ソースごとに表示されます',
'aliases.effort.title': 'エイリアスオプション',
'aliases.effort.description': '推論レベル、カスタム値、または Fast 速度階層を選択します',
'aliases.effort.title': '推論レベル',
'aliases.effort.description': '個別に設定することも、Fast と同時に使用することもできます',
'aliases.effort.none': '設定しない',
'aliases.effort.noneHint': '推論レベルを上書きせず、ほかの有効なオプションのみ使用します',
'aliases.effort.unsupported': 'このモデルには利用可能な推論レベルがないため、Fast のみ有効にしました。',
'aliases.fast.title': '速度オプション',
'aliases.fast.description': 'Fast リクエスト階層を固定します(service_tier=priority)',
'aliases.fast.enabled': '有効:推論レベルと同時に適用されます',
'aliases.fast.disabled': '無効',
'aliases.customLevel': 'カスタムレベル:{effort}',
'aliases.customLevelHint': 'モデルプロバイダーが対応する別の推論レベルを入力',
'aliases.custom': 'カスタム',
@@ -235,7 +244,7 @@ export const jaOverrides = {
'aliases.creating': '作成中',
'aliases.create': 'エイリアスを作成',
'aliases.createdList.title': '作成済みエイリアス',
'aliases.createdList.description': '推論エイリアスと Fast エイリアスを一つの一覧で管理します',
'aliases.createdList.description': '各エイリアスの推論レベルと Fast 状態をまとめて表示します',
'aliases.loadingConfig': '設定を読み込んでいます',
'aliases.empty.title': 'モデルエイリアスはまだありません',
'aliases.empty.description': '左側でモデルとエイリアスオプションを選択して作成できます',
+29 -11
View File
@@ -206,11 +206,14 @@ export const zhCN = {
'aliases.error.selectModel': '请先选择原模型',
'aliases.error.emptyAlias': '别名模型不能为空',
'aliases.error.emptyEffort': '思考强度不能为空',
'aliases.error.emptyOptions': '请至少设置思考强度或启用 Fast',
'aliases.error.unsupportedEffort': '该模型不支持设置思考强度,只能使用 Fast 选项',
'aliases.created': '已创建 {alias},思考强度固定为 {effort}',
'aliases.deleteConfirm': '确定删除别名模型“{alias}”吗?\n将同时移除对应的思考强度覆盖规则。',
'aliases.createdCombined': '已创建 {alias},思考强度为 {effort},同时启用 Fast',
'aliases.deleteConfirm': '确定删除别名模型“{alias}”吗?\n将同时移除对应的思考强度和 Fast 覆盖规则。',
'aliases.deleted': '已删除 {alias}',
'aliases.create.title': '创建模型别名',
'aliases.create.description': '统一设置思考强度或 Fast 速度层级',
'aliases.create.description': '思考强度和 Fast 速度层级可独立设置,也可以同时启用',
'aliases.originalModel': '原模型',
'aliases.loadingModels': '正在读取模型',
'aliases.searchModel': '搜索并选择原模型',
@@ -219,9 +222,15 @@ export const zhCN = {
'aliases.noModels': '当前没有可选模型',
'aliases.sourceLabel': '来源:{source}',
'aliases.sourceHint': '同名模型会按 Codex OAuth、Codex API 和 OpenAI 兼容来源分别列出',
'aliases.effort.title': '别名选项',
'aliases.effort.description': '选择思考强度、自定义等级或 Fast 速度层级',
'aliases.effort.title': '思考强度',
'aliases.effort.description': '可单独设置,也可以与 Fast 同时启用',
'aliases.effort.none': '不设置',
'aliases.effort.noneHint': '不覆盖思考强度,仅使用其他已启用的选项',
'aliases.effort.unsupported': '这个模型没有可用的思考强度,已切换为仅启用 Fast。',
'aliases.fast.title': '速度选项',
'aliases.fast.description': '固定使用 Fast 请求层级(service_tier=priority)',
'aliases.fast.enabled': '已启用,可与思考强度同时生效',
'aliases.fast.disabled': '未启用',
'aliases.customLevel': '自定义等级:{effort}',
'aliases.customLevelHint': '输入模型提供商支持的其他思考等级',
'aliases.custom': '自定义',
@@ -238,7 +247,7 @@ export const zhCN = {
'aliases.creating': '创建中',
'aliases.create': '创建别名',
'aliases.createdList.title': '已创建别名',
'aliases.createdList.description': '统一显示思考强度别名和 Fast 速度别名',
'aliases.createdList.description': '每个别名会同时显示已设置的思考强度和 Fast 状态',
'aliases.loadingConfig': '读取配置中',
'aliases.empty.title': '尚未创建模型别名',
'aliases.empty.description': '从左侧选择模型和别名选项即可创建',
@@ -321,7 +330,7 @@ export const zhCN = {
'config.network.port': '端口',
'config.network.portHint': '范围 1–65535;内核运行时修改会自动重启',
'config.network.allowLan': '允许局域网',
'config.network.allowLanHint': '启用后将',
'config.network.allowLanHint': '启用后将监听0.0.0.0',
'config.network.proxyUrl': '代理 URL',
'config.network.proxyPlaceholder': '例如 socks5://127.0.0.1:7890',
'config.network.proxyHint': '全局上游代理;留空表示不使用',
@@ -1087,11 +1096,14 @@ export const en: Record<MessageKey, string> = {
'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<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': '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<MessageKey, string> = {
'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',
+141 -78
View File
@@ -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<string, AliasListEntry>();
const entryKey = (entry: Pick<ThinkingAliasEntry, 'kind' | 'provider' | 'sourceModel' | 'alias'>) => (
[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<string, ModelAliasSource>();
[...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<ThinkingAliasSource[]>([]);
const [speedSources, setSpeedSources] = useState<ThinkingAliasSource[]>([]);
const [selectedSourceId, setSelectedSourceId] = useState('');
const [aliasMode, setAliasMode] = useState<AliasMode>('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<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', {
if (normalizedEffort) {
await invoke<ThinkingAliasEntry[]>('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<SpeedAliasEntry[]>('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<SpeedAliasEntry[]>('delete_speed_alias', {
if (entry.effort) {
await invoke<ThinkingAliasEntry[]>('delete_thinking_alias', {
alias: entry.alias,
});
setSpeedEntries(nextEntries);
setNotice(t('speedAliases.deleted', { alias: entry.alias }));
} else {
const nextEntries = await invoke<ThinkingAliasEntry[]>('delete_thinking_alias', {
await invoke<SpeedAliasEntry[]>('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() {
<span>{t('aliases.effort.description')}</span>
</div>
<div className="thinking-effort-options">
<button
type="button"
className={!normalizedEffort ? 'active none' : 'none'}
onClick={() => {
chooseEffort('');
setCustomEffortOpen(false);
}}
disabled={Boolean(busyAlias)}
title={t('aliases.effort.noneHint')}
>
{t('aliases.effort.none')}
</button>
{effortOptions.map((option) => (
<button
type="button"
className={aliasMode === 'reasoning' && effort.trim().toLowerCase() === option.value ? 'active' : ''}
className={effort.trim().toLowerCase() === option.value ? 'active' : ''}
key={option.value}
onClick={() => {
setAliasMode('reasoning');
chooseEffort(option.value);
setCustomEffortOpen(false);
}}
disabled={Boolean(busyAlias)}
disabled={Boolean(busyAlias) || Boolean(selectedSource && !selectedSource.supportsReasoning)}
title={t(option.hintKey)}
>
{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);
}}
disabled={Boolean(busyAlias)}
disabled={Boolean(busyAlias) || Boolean(selectedSource && !selectedSource.supportsReasoning)}
title={customEffortSelected
? t('aliases.customLevel', { effort: normalizedEffort })
: t('aliases.customLevelHint')}
@@ -479,10 +521,7 @@ export function ThinkingAliasesPage() {
<input
id="thinking-effort-custom"
value={customEffortSelected ? effort : ''}
onChange={(event) => {
setAliasMode('reasoning');
chooseEffort(event.currentTarget.value);
}}
onChange={(event) => chooseEffort(event.currentTarget.value)}
placeholder={t('aliases.customPlaceholder')}
maxLength={64}
spellCheck={false}
@@ -500,6 +539,26 @@ export function ThinkingAliasesPage() {
</button>
<small>{t('aliases.customHelp')}</small>
</div> : null}
{selectedSource && !selectedSource.supportsReasoning ? (
<small className="thinking-model-hint">{t('aliases.effort.unsupported')}</small>
) : null}
</div>
<div className="thinking-alias-field">
<div className="thinking-field-heading">
<strong>{t('aliases.fast.title')}</strong>
<span>{t('aliases.fast.description')}</span>
</div>
<button
type="button"
className={`thinking-speed-toggle${fastEnabled ? ' active' : ''}`}
aria-pressed={fastEnabled}
onClick={() => setFastEnabled((current) => !current)}
disabled={Boolean(busyAlias)}
>
<span>Fast</span>
<small>{fastEnabled ? t('aliases.fast.enabled') : t('aliases.fast.disabled')}</small>
</button>
</div>
<div className="thinking-alias-section-divider" aria-hidden="true" />
@@ -514,21 +573,24 @@ export function ThinkingAliasesPage() {
value={alias}
onChange={(event) => setAlias(event.currentTarget.value)}
placeholder={selectedSource
? aliasMode === 'fast'
? t('speedAliases.aliasName.example', { model: selectedSource.model })
: t('aliases.aliasName.example', { model: selectedSource.model, effort: normalizedEffort || 'high' })
? normalizedEffort
? t('aliases.aliasName.example', { model: selectedSource.model, effort: normalizedEffort })
: t('speedAliases.aliasName.example', { model: selectedSource.model })
: t('aliases.aliasName.selectFirst')}
disabled={Boolean(busyAlias)}
/>
</div>
<div className="thinking-alias-preview">
{aliasMode === 'fast' ? <Zap size={18} /> : <BrainCircuit size={18} />}
{normalizedEffort ? <BrainCircuit size={18} /> : <Zap size={18} />}
<div>
<span>{selectedSource?.model || t('aliases.notSelected')} <ArrowRight size={13} /> {alias || t('aliases.enterAlias')}</span>
<code>{aliasMode === 'fast'
? 'service_tier = priority'
: `${selectedSource?.protocol === 'openai' ? 'reasoning_effort' : 'reasoning.effort'} = ${normalizedEffort || t('aliases.notSet')}`}</code>
<code>{[
normalizedEffort
? `${selectedSource?.protocol === 'openai' ? 'reasoning_effort' : 'reasoning.effort'} = ${normalizedEffort}`
: '',
fastEnabled ? 'service_tier = priority' : '',
].filter(Boolean).join(' · ') || t('aliases.notSet')}</code>
</div>
</div>
@@ -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'
? <LoaderCircle size={16} className="spin" />
: aliasMode === 'fast' ? <Zap size={16} /> : <GitFork size={16} />}
: <GitFork size={16} />}
{busyAction === 'create' ? t('aliases.creating') : t('aliases.create')}
</button>
</section>
@@ -578,19 +640,20 @@ export function ThinkingAliasesPage() {
<ArrowRight size={14} />
<strong title={entry.alias}>{entry.alias}</strong>
</div>
<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>
<div className="thinking-alias-badges">
{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"
className="icon-button danger"
onClick={() => void deleteAlias(entry)}
disabled={Boolean(busyAlias)}
title={t(entry.mode === 'fast' ? 'speedAliases.delete' : 'aliases.delete', {
alias: entry.alias,
})}
title={t('aliases.delete', { alias: entry.alias })}
>
{busyAction === 'delete' && busyAlias === entry.alias
? <LoaderCircle size={15} className="spin" />
+39 -7
View File
@@ -4051,19 +4051,44 @@ 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:disabled {
cursor: not-allowed;
opacity: 0.45;
}
.thinking-effort-options button.fast.active {
.thinking-speed-toggle {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
min-height: 42px;
padding: 7px 10px;
border: 1px solid var(--theme-d5d2cb);
border-radius: 7px;
background: var(--theme-fff);
color: var(--theme-7a746c);
}
.thinking-speed-toggle span {
font-size: var(--font-size-section);
font-weight: 800;
}
.thinking-speed-toggle small {
color: var(--theme-9a938a);
font-size: var(--font-size-label);
}
.thinking-speed-toggle.active {
border-color: var(--theme-d7a447);
background: var(--theme-fff8eb);
color: var(--theme-805a24);
}
.thinking-speed-toggle.active small {
color: var(--theme-805a24);
}
.thinking-effort-custom {
display: grid;
grid-template-columns: minmax(0, 1fr) 30px;
@@ -4283,6 +4308,13 @@ textarea:focus-visible {
text-transform: uppercase;
}
.thinking-alias-badges {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 5px;
}
.thinking-effort-badge.missing {
border-color: var(--theme-e4d0af);
background: var(--theme-fff8eb);
@@ -4306,7 +4338,7 @@ textarea:focus-visible {
grid-column: 1 / -1;
}
.thinking-alias-list-panel .thinking-effort-badge {
.thinking-alias-list-panel .thinking-alias-badges {
grid-column: 2;
}
+41 -3
View File
@@ -1,6 +1,7 @@
import { describe, expect, test } from 'bun:test';
import {
combineModelAliasEntries,
combineModelAliasSources,
thinkingAliasSourceKindLabel,
} from '../src/pages/ThinkingAliasesPage';
@@ -32,9 +33,46 @@ describe('统一模型别名列表', () => {
}],
);
expect(entries.map((entry) => [entry.alias, entry.mode])).toEqual([
['gpt-fast', 'fast'],
['gpt-high', 'reasoning'],
expect(entries.map((entry) => [entry.alias, entry.effort, entry.serviceTier])).toEqual([
['gpt-fast', null, 'priority'],
['gpt-high', 'high', null],
]);
});
test('同一个别名可同时设置思考强度和 Fast', () => {
const identity = {
sourceModel: 'gpt-5.6-sol',
alias: 'gpt-5.6-sol-xhigh-fast',
provider: 'Codex OAuth',
kind: 'codex-oauth',
};
const entries = combineModelAliasEntries(
[{ ...identity, effort: 'xhigh' }],
[{ ...identity, serviceTier: 'priority' }],
);
expect(entries).toHaveLength(1);
expect(entries[0]).toMatchObject({ effort: 'xhigh', serviceTier: 'priority' });
});
test('速度模型与思考模型合并后保留思考能力标记', () => {
const baseSource = {
model: 'gpt-5.6-sol',
displayName: 'GPT-5.6 Sol',
provider: 'Codex OAuth',
kind: 'codex-oauth',
protocol: 'codex',
};
const sources = combineModelAliasSources(
[{ ...baseSource, id: 'reasoning-and-fast' }],
[
{ ...baseSource, id: 'reasoning-and-fast' },
{ ...baseSource, id: 'fast-only', model: 'gpt-speed-only' },
],
);
expect(sources).toHaveLength(2);
expect(sources.find((source) => source.id === 'reasoning-and-fast')?.supportsReasoning).toBe(true);
expect(sources.find((source) => source.id === 'fast-only')?.supportsReasoning).toBe(false);
});
});