Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 38 additions & 10 deletions src-tauri/src/agents/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,7 @@ pub(crate) async fn create_thinking_alias(
alias: String,
effort: String,
fast: Option<bool>,
original_alias: Option<String>,
) -> Result<Vec<ThinkingAliasEntry>, String> {
let config = gui_config_state.snapshot()?;
let source_id = source_id.trim().to_string();
Expand All @@ -518,6 +519,10 @@ pub(crate) async fn create_thinking_alias(
};
let fast = fast.unwrap_or(false);
let content = fetch_management_config_yaml(&config).await?;
let working_content = match original_alias.as_deref() {
Some(original) => prepare_model_alias_edit(&content, original)?,
None => content.clone(),
};
let available_models =
fetch_agent_models(config.port, effective_agent_api_key(&config)).await?;
let definitions = fetch_oauth_model_definitions(&config).await;
Expand All @@ -528,12 +533,33 @@ pub(crate) async fn create_thinking_alias(
} else {
AliasSourceCapability::Base
};
let sources =
let sources = resolved_oauth_alias_sources(
&working_content,
&definitions,
&available_models,
capability,
)?;
// Removing a configured alias can shift model indices in source IDs.
let original_sources =
resolved_oauth_alias_sources(&content, &definitions, &available_models, capability)?;
let source = sources
let original_source = original_sources
.iter()
.find(|source| source.source.id == source_id)
.cloned()
.find(|source| source.source.id == source_id);
let candidates = sources
.iter()
.filter(|source| {
if original_alias.is_none() {
return source.source.id == source_id;
}
original_source.is_some_and(|original| {
source.source.model == original.source.model
&& source.source.kind == original.source.kind
&& source.source.provider == original.source.provider
})
})
.collect::<Vec<_>>();
let source = (candidates.len() == 1)
.then(|| candidates[0].clone())
.ok_or_else(|| {
"原模型已不在内核当前可用模型中,或其配置来源已经变化,请刷新后重新选择".to_string()
})?;
Expand All @@ -556,13 +582,15 @@ pub(crate) async fn create_thinking_alias(
return Err("别名模型不能和原模型相同".to_string());
}

if available_models
.iter()
.any(|model| model.name.eq_ignore_ascii_case(&alias))
{
if available_models.iter().any(|model| {
model.name.eq_ignore_ascii_case(&alias)
&& !original_alias
.as_deref()
.is_some_and(|original| original.eq_ignore_ascii_case(&alias))
}) {
return Err(format!("{alias} 已经是实际模型 ID,不能再作为别名"));
}
let document = serde_norway::from_str::<serde_norway::Value>(&content)
let document = serde_norway::from_str::<serde_norway::Value>(&working_content)
.map_err(|error| format!("解析内核 YAML 配置失败: {error}"))?;
let root = document
.as_mapping()
Expand All @@ -571,7 +599,7 @@ pub(crate) async fn create_thinking_alias(
return Err(format!("别名模型 {alias} 已存在"));
}

let updated = add_model_alias_to_yaml(&content, &source, &alias, &effort, fast)?;
let updated = add_model_alias_to_yaml(&working_content, &source, &alias, &effort, fast)?;
put_management_alias_config_changes(&config, &content, &updated).await?;
thinking_aliases_from_yaml(&updated)
}
Expand Down
40 changes: 40 additions & 0 deletions src-tauri/src/core_config/aliases.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1619,6 +1619,46 @@ pub(crate) fn insert_thinking_effort_params(
Ok(())
}

// Build the replacement in memory; callers only persist after all validation succeeds.
pub(crate) fn prepare_model_alias_edit(content: &str, alias: &str) -> Result<String, String> {
let mut document = yaml_serde_edit::YamlValue::parse(content)
.map_err(|error| format!("解析内核 YAML 配置失败: {error}"))?;
let mut updated = document.get().clone();
let root = updated
.as_mapping_mut()
.ok_or_else(|| "内核配置顶层必须是 YAML 映射".to_string())?;
let matches = |model: &&serde_norway::Value| {
configured_model_identity(model)
.is_some_and(|(_, name, _)| name.eq_ignore_ascii_case(alias))
};
let api_count = MODEL_ALIAS_CONFIG_SECTIONS
.into_iter()
.filter_map(|section| yaml_mapping_value(root, section))
.filter_map(serde_norway::Value::as_sequence)
.flatten()
.filter_map(serde_norway::Value::as_mapping)
.filter_map(|provider| yaml_mapping_value(provider, "models"))
.filter_map(serde_norway::Value::as_sequence)
.flatten()
.filter(matches)
.count();
let oauth_count = yaml_mapping_value(root, "oauth-model-alias")
.and_then(serde_norway::Value::as_mapping)
.into_iter()
.flat_map(|channels| channels.values())
.filter_map(serde_norway::Value::as_sequence)
.flatten()
.filter(matches)
.count();
if api_count + oauth_count != 1 {
return Err("别名不存在或存在多个同名映射,请刷新并检查配置后重试".to_string());
}
remove_existing_claude_model_alias(root, alias)?;
remove_thinking_payload_model(root, alias)?;
remove_speed_payload_model(root, alias)?;
render_updated_core_yaml(&mut document, updated)
}

pub(crate) fn add_model_alias_to_yaml(
content: &str,
source: &ResolvedThinkingAliasSource,
Expand Down
29 changes: 29 additions & 0 deletions src-tauri/src/tests/model_aliases.rs
Original file line number Diff line number Diff line change
Expand Up @@ -671,3 +671,32 @@ fn speed_alias_supports_codex_api_model_entries() {
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].kind, "codex-api");
}

#[test]
fn edit_model_alias_replaces_name_effort_and_fast_in_memory() {
let source = test_oauth_thinking_source("codex", "gpt-test");
let original =
add_model_alias_to_yaml("port: 8317\n", &source, "old-alias", "high", true).unwrap();
let prepared = prepare_model_alias_edit(&original, "old-alias").unwrap();
let updated = add_model_alias_to_yaml(&prepared, &source, "new-alias", "low", false).unwrap();
let entries = thinking_aliases_from_yaml(&updated).unwrap();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].alias, "new-alias");
assert_eq!(entries[0].effort.as_deref(), Some("low"));
assert!(speed_aliases_from_yaml(&updated).unwrap().is_empty());
assert!(updated.contains("port: 8317"));
assert!(!updated.contains("old-alias"));
let unchanged_name =
add_model_alias_to_yaml(&prepared, &source, "old-alias", "", false).unwrap();
assert_eq!(
thinking_aliases_from_yaml(&unchanged_name).unwrap()[0].effort,
None
);
}

#[test]
fn edit_model_alias_rejects_missing_and_ambiguous_aliases() {
assert!(prepare_model_alias_edit("port: 8317\n", "missing").is_err());
let content = "oauth-model-alias:\n codex:\n - name: model-a\n alias: shared\n claude:\n - name: model-b\n alias: shared\n";
assert!(prepare_model_alias_edit(content, "shared").is_err());
}
2 changes: 1 addition & 1 deletion src/i18n/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -436,7 +436,7 @@ export const en: Record<MessageKey, string> = {
'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.empty.description': 'Click “Create Alias” to add your first model alias',
'aliases.unboundEffort': 'No effort bound',
'aliases.delete': 'Delete {alias}',
'speedAliases.created': 'Created {alias} with the Fast request tier fixed',
Expand Down
2 changes: 1 addition & 1 deletion src/i18n/locales/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,7 @@ export const zhCN = {
'aliases.createdList.description': '每个别名会显示已固定的思考强度和 Fast 状态',
'aliases.loadingConfig': '读取配置中',
'aliases.empty.title': '尚未创建模型别名',
'aliases.empty.description': '从左侧选择模型和别名选项即可创建',
'aliases.empty.description': '点击“创建别名”添加第一个模型别名',
'aliases.unboundEffort': '未绑定强度',
'aliases.delete': '删除 {alias}',
'speedAliases.created': '已创建 {alias},并固定使用 Fast 请求层级',
Expand Down
Loading