fix(aliases): allow reasoning effort and Fast mode together
This commit is contained in:
1 parent
aa8a7cbd84
commit
eaefcd6d9b
6 files changed
+307
-105
No files matched your search
+43
-1
@@ -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
@@ -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
@@ -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',
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user