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:
1 parent
708a834e24
commit
cff6a21974
9 files changed
+443
-112
No files matched your search
@@ -437,6 +437,7 @@ pub(crate) 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();
|
||||
@@ -449,12 +450,15 @@ pub(crate) async fn create_thinking_alias(
|
||||
} else {
|
||||
validate_thinking_alias_effort(&effort)?
|
||||
};
|
||||
let fast = fast.unwrap_or(false);
|
||||
let content = fetch_management_config_yaml(&config).await?;
|
||||
let available_models =
|
||||
fetch_agent_models(config.port, effective_agent_api_key(&config)).await?;
|
||||
let definitions = fetch_oauth_model_definitions(&config).await;
|
||||
let capability = if !effort.is_empty() {
|
||||
AliasSourceCapability::Reasoning
|
||||
} else if fast {
|
||||
AliasSourceCapability::Fast
|
||||
} else {
|
||||
AliasSourceCapability::Base
|
||||
};
|
||||
@@ -467,6 +471,9 @@ pub(crate) async fn create_thinking_alias(
|
||||
.ok_or_else(|| {
|
||||
"原模型已不在内核当前可用模型中,或其配置来源已经变化,请刷新后重新选择".to_string()
|
||||
})?;
|
||||
if fast && !alias_source_supports_fast(&source) {
|
||||
return Err("Fast 仅支持 OpenAI 兼容 API、Codex API 或 Codex OAuth 模型源".to_string());
|
||||
}
|
||||
if !effort.is_empty()
|
||||
&& !source
|
||||
.source
|
||||
@@ -498,7 +505,7 @@ pub(crate) async fn create_thinking_alias(
|
||||
return Err(format!("别名模型 {alias} 已存在"));
|
||||
}
|
||||
|
||||
let updated = add_model_alias_to_yaml(&content, &source, &alias, &effort)?;
|
||||
let updated = add_model_alias_to_yaml(&content, &source, &alias, &effort, fast)?;
|
||||
put_management_alias_config_changes(&config, &content, &updated).await?;
|
||||
thinking_aliases_from_yaml(&updated)
|
||||
}
|
||||
|
||||
@@ -922,9 +922,7 @@ pub(crate) fn resolved_oauth_alias_sources(
|
||||
AliasSourceCapability::Reasoning => {
|
||||
sources.retain(|source| !source.source.reasoning_levels.is_empty())
|
||||
}
|
||||
AliasSourceCapability::Fast => {
|
||||
sources.retain(|source| model_supports_fast(&source.source.model))
|
||||
}
|
||||
AliasSourceCapability::Fast => sources.retain(alias_source_supports_fast),
|
||||
AliasSourceCapability::Base => {}
|
||||
}
|
||||
let configured_codex_api_models = sources
|
||||
@@ -949,8 +947,6 @@ pub(crate) fn resolved_oauth_alias_sources(
|
||||
.filter(|definition| {
|
||||
(capability != AliasSourceCapability::Reasoning
|
||||
|| !definition.reasoning_levels.is_empty())
|
||||
&& (capability != AliasSourceCapability::Fast
|
||||
|| model_supports_fast(&definition.id))
|
||||
&& thinking_alias_model_is_available(available_models, &definition.id)
|
||||
&& (channel.key != "codex"
|
||||
|| !configured_codex_api_models
|
||||
@@ -985,19 +981,13 @@ pub(crate) fn thinking_alias_model_is_available(
|
||||
.any(|available| available.name.eq_ignore_ascii_case(model))
|
||||
}
|
||||
|
||||
pub(crate) fn model_supports_fast(model: &str) -> bool {
|
||||
let model = model.trim().to_ascii_lowercase();
|
||||
model == "gpt" || model.starts_with("gpt-")
|
||||
}
|
||||
|
||||
pub(crate) fn alias_source_supports_fast(source: &ResolvedThinkingAliasSource) -> bool {
|
||||
model_supports_fast(&source.source.model)
|
||||
&& match &source.location {
|
||||
ThinkingAliasSourceLocation::Oauth { channel, .. } => *channel == "codex",
|
||||
ThinkingAliasSourceLocation::ConfigModel { section, .. } => {
|
||||
matches!(*section, "codex-api-key" | "openai-compatibility")
|
||||
}
|
||||
match &source.location {
|
||||
ThinkingAliasSourceLocation::Oauth { channel, .. } => *channel == "codex",
|
||||
ThinkingAliasSourceLocation::ConfigModel { section, .. } => {
|
||||
matches!(*section, "codex-api-key" | "openai-compatibility")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn collect_config_thinking_alias_sources(
|
||||
@@ -1634,7 +1624,11 @@ pub(crate) fn add_model_alias_to_yaml(
|
||||
source: &ResolvedThinkingAliasSource,
|
||||
alias: &str,
|
||||
effort: &str,
|
||||
fast: bool,
|
||||
) -> Result<String, String> {
|
||||
if fast && !alias_source_supports_fast(source) {
|
||||
return Err("Fast 仅支持 OpenAI 兼容 API、Codex API 或 Codex OAuth 模型源".to_string());
|
||||
}
|
||||
let mut document = yaml_serde_edit::YamlValue::parse(content)
|
||||
.map_err(|error| format!("解析内核 YAML 配置失败: {error}"))?;
|
||||
let mut updated = document.get().clone();
|
||||
@@ -1667,8 +1661,32 @@ pub(crate) fn add_model_alias_to_yaml(
|
||||
}
|
||||
|
||||
remove_thinking_payload_model(root, alias)?;
|
||||
if effort.is_empty() {
|
||||
return render_updated_core_yaml(&mut document, updated);
|
||||
remove_speed_payload_model(root, alias)?;
|
||||
if !effort.is_empty() {
|
||||
let mut params_mapping = serde_norway::Mapping::new();
|
||||
insert_thinking_effort_params(&mut params_mapping, &source.source, effort)?;
|
||||
append_alias_payload_override(root, alias, &source.source.protocol, params_mapping)?;
|
||||
}
|
||||
if fast {
|
||||
let mut params_mapping = serde_norway::Mapping::new();
|
||||
params_mapping.insert(
|
||||
yaml_key("service_tier"),
|
||||
serde_norway::Value::String("priority".to_string()),
|
||||
);
|
||||
append_alias_payload_override(root, alias, &source.source.protocol, params_mapping)?;
|
||||
}
|
||||
|
||||
render_updated_core_yaml(&mut document, updated)
|
||||
}
|
||||
|
||||
pub(crate) fn append_alias_payload_override(
|
||||
root: &mut serde_norway::Mapping,
|
||||
alias: &str,
|
||||
protocol: &str,
|
||||
params_mapping: serde_norway::Mapping,
|
||||
) -> Result<(), String> {
|
||||
if params_mapping.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let payload = root
|
||||
.entry(yaml_key("payload"))
|
||||
@@ -1688,12 +1706,8 @@ pub(crate) fn add_model_alias_to_yaml(
|
||||
);
|
||||
model_mapping.insert(
|
||||
yaml_key("protocol"),
|
||||
serde_norway::Value::String(source.source.protocol.clone()),
|
||||
serde_norway::Value::String(protocol.to_string()),
|
||||
);
|
||||
let mut params_mapping = serde_norway::Mapping::new();
|
||||
if !effort.is_empty() {
|
||||
insert_thinking_effort_params(&mut params_mapping, &source.source, effort)?;
|
||||
}
|
||||
let mut rule_mapping = serde_norway::Mapping::new();
|
||||
rule_mapping.insert(
|
||||
yaml_key("models"),
|
||||
@@ -1704,8 +1718,7 @@ pub(crate) fn add_model_alias_to_yaml(
|
||||
serde_norway::Value::Mapping(params_mapping),
|
||||
);
|
||||
override_rules.push(serde_norway::Value::Mapping(rule_mapping));
|
||||
|
||||
render_updated_core_yaml(&mut document, updated)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn add_speed_alias_to_yaml(
|
||||
@@ -1714,9 +1727,7 @@ pub(crate) fn add_speed_alias_to_yaml(
|
||||
alias: &str,
|
||||
) -> Result<String, String> {
|
||||
if !alias_source_supports_fast(source) {
|
||||
return Err(
|
||||
"Fast 仅支持 OpenAI 兼容 API、Codex API 或 Codex OAuth 的 GPT 系列模型".to_string(),
|
||||
);
|
||||
return Err("Fast 仅支持 OpenAI 兼容 API、Codex API 或 Codex OAuth 模型源".to_string());
|
||||
}
|
||||
let mut document = yaml_serde_edit::YamlValue::parse(content)
|
||||
.map_err(|error| format!("解析内核 YAML 配置失败: {error}"))?;
|
||||
@@ -1749,41 +1760,12 @@ pub(crate) fn add_speed_alias_to_yaml(
|
||||
}
|
||||
|
||||
remove_speed_payload_model(root, alias)?;
|
||||
let payload = root
|
||||
.entry(yaml_key("payload"))
|
||||
.or_insert_with(|| serde_norway::Value::Mapping(serde_norway::Mapping::new()))
|
||||
.as_mapping_mut()
|
||||
.ok_or_else(|| "payload 必须是 YAML 映射".to_string())?;
|
||||
let override_rules = payload
|
||||
.entry(yaml_key("override"))
|
||||
.or_insert_with(|| serde_norway::Value::Sequence(Vec::new()))
|
||||
.as_sequence_mut()
|
||||
.ok_or_else(|| "payload.override 必须是数组".to_string())?;
|
||||
|
||||
let mut model_mapping = serde_norway::Mapping::new();
|
||||
model_mapping.insert(
|
||||
yaml_key("name"),
|
||||
serde_norway::Value::String(alias.to_string()),
|
||||
);
|
||||
model_mapping.insert(
|
||||
yaml_key("protocol"),
|
||||
serde_norway::Value::String(source.source.protocol.clone()),
|
||||
);
|
||||
let mut params_mapping = serde_norway::Mapping::new();
|
||||
params_mapping.insert(
|
||||
yaml_key("service_tier"),
|
||||
serde_norway::Value::String("priority".to_string()),
|
||||
);
|
||||
let mut rule_mapping = serde_norway::Mapping::new();
|
||||
rule_mapping.insert(
|
||||
yaml_key("models"),
|
||||
serde_norway::Value::Sequence(vec![serde_norway::Value::Mapping(model_mapping)]),
|
||||
);
|
||||
rule_mapping.insert(
|
||||
yaml_key("params"),
|
||||
serde_norway::Value::Mapping(params_mapping),
|
||||
);
|
||||
override_rules.push(serde_norway::Value::Mapping(rule_mapping));
|
||||
append_alias_payload_override(root, alias, &source.source.protocol, params_mapping)?;
|
||||
|
||||
render_updated_core_yaml(&mut document, updated)
|
||||
}
|
||||
|
||||
@@ -2214,7 +2214,7 @@ fn thinking_alias_prefers_codex_api_key_model_over_same_named_oauth_definition()
|
||||
));
|
||||
|
||||
let rendered =
|
||||
add_model_alias_to_yaml(input, &sources[0], "gpt-5.6-luna-xhigh", "xhigh").unwrap();
|
||||
add_model_alias_to_yaml(input, &sources[0], "gpt-5.6-luna-xhigh", "xhigh", false).unwrap();
|
||||
assert!(rendered.contains("alias: gpt-5.6-luna-xhigh"), "{rendered}");
|
||||
assert!(!rendered.contains("oauth-model-alias"), "{rendered}");
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ fn thinking_alias_uses_source_native_override_parameters() {
|
||||
for (channel, model, effort, expected) in cases {
|
||||
let source = test_oauth_thinking_source(channel, model);
|
||||
let alias = format!("{model}-{effort}");
|
||||
let rendered = add_model_alias_to_yaml("{}\n", &source, &alias, effort).unwrap();
|
||||
let rendered = add_model_alias_to_yaml("{}\n", &source, &alias, effort, false).unwrap();
|
||||
for parameter in expected {
|
||||
assert!(
|
||||
rendered.contains(parameter),
|
||||
@@ -187,7 +187,8 @@ fn configured_claude_and_gemini_models_use_native_overrides() {
|
||||
.iter()
|
||||
.find(|source| source.source.kind == "claude-api")
|
||||
.unwrap();
|
||||
let with_claude = add_model_alias_to_yaml(input, claude, "claude-fixed", "high").unwrap();
|
||||
let with_claude =
|
||||
add_model_alias_to_yaml(input, claude, "claude-fixed", "high", false).unwrap();
|
||||
assert!(
|
||||
with_claude.contains("output_config.effort: high"),
|
||||
"{with_claude}"
|
||||
@@ -204,7 +205,8 @@ fn configured_claude_and_gemini_models_use_native_overrides() {
|
||||
.iter()
|
||||
.find(|source| source.source.kind == "gemini-api")
|
||||
.unwrap();
|
||||
let rendered = add_model_alias_to_yaml(&with_claude, gemini, "gemini-fixed", "low").unwrap();
|
||||
let rendered =
|
||||
add_model_alias_to_yaml(&with_claude, gemini, "gemini-fixed", "low", false).unwrap();
|
||||
assert!(
|
||||
rendered.contains("generationConfig.thinkingConfig.thinkingLevel: low"),
|
||||
"{rendered}"
|
||||
@@ -260,7 +262,7 @@ fn antigravity_alias_uses_its_own_oauth_channel_and_force_mapping() {
|
||||
assert_eq!(sources.len(), 1);
|
||||
assert_eq!(sources[0].source.kind, "antigravity-oauth");
|
||||
let rendered =
|
||||
add_model_alias_to_yaml("{}\n", &sources[0], "gemini-3.1-pro-preview", "").unwrap();
|
||||
add_model_alias_to_yaml("{}\n", &sources[0], "gemini-3.1-pro-preview", "", false).unwrap();
|
||||
|
||||
assert!(rendered.contains("antigravity:"), "{rendered}");
|
||||
assert!(rendered.contains("name: gemini-pro-agent"), "{rendered}");
|
||||
@@ -291,15 +293,20 @@ fn oauth_alias_reader_and_delete_preserve_the_exact_channel() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fast_is_only_available_for_gpt_models_from_supported_sources() {
|
||||
fn fast_is_only_available_for_supported_model_sources() {
|
||||
let input = "openai-compatibility:\n - name: Relay\n base-url: https://example.com/v1\n models:\n - name: gpt-test\n - name: deepseek-chat\n";
|
||||
let available_models = test_agent_models(&["gpt-test", "deepseek-chat"]);
|
||||
let sources =
|
||||
resolved_oauth_alias_sources(input, &[], &available_models, AliasSourceCapability::Fast)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(sources.len(), 1);
|
||||
assert_eq!(sources[0].source.model, "gpt-test");
|
||||
assert_eq!(sources.len(), 2);
|
||||
assert!(sources
|
||||
.iter()
|
||||
.any(|source| source.source.model == "gpt-test"));
|
||||
assert!(sources
|
||||
.iter()
|
||||
.any(|source| source.source.model == "deepseek-chat"));
|
||||
|
||||
let deepseek_source = ResolvedThinkingAliasSource {
|
||||
source: ThinkingAliasSource {
|
||||
@@ -317,7 +324,11 @@ fn fast_is_only_available_for_gpt_models_from_supported_sources() {
|
||||
model_index: 1,
|
||||
},
|
||||
};
|
||||
assert!(add_speed_alias_to_yaml(input, &deepseek_source, "deepseek-fast").is_err());
|
||||
let deepseek_fast = add_speed_alias_to_yaml(input, &deepseek_source, "deepseek-fast").unwrap();
|
||||
assert!(
|
||||
deepseek_fast.contains("service_tier: priority"),
|
||||
"{deepseek_fast}"
|
||||
);
|
||||
|
||||
let antigravity_source = ResolvedThinkingAliasSource {
|
||||
source: ThinkingAliasSource {
|
||||
@@ -349,7 +360,8 @@ fn fast_is_only_available_for_gpt_models_from_supported_sources() {
|
||||
fn thinking_alias_adds_fork_and_matching_payload_rule() {
|
||||
let input = "# Keep this comment\ndebug: true\npayload:\n override:\n - models:\n - name: existing-fast\n protocol: codex\n params:\n service_tier: priority\n";
|
||||
let source = test_codex_oauth_thinking_source("gpt-5.5");
|
||||
let rendered = add_model_alias_to_yaml(input, &source, "gpt-5.5-xhigh", "xhigh").unwrap();
|
||||
let rendered =
|
||||
add_model_alias_to_yaml(input, &source, "gpt-5.5-xhigh", "xhigh", false).unwrap();
|
||||
let aliases = thinking_aliases_from_yaml(&rendered).unwrap();
|
||||
|
||||
assert!(rendered.contains("# Keep this comment"), "{rendered}");
|
||||
@@ -370,7 +382,7 @@ fn thinking_alias_adds_fork_and_matching_payload_rule() {
|
||||
#[test]
|
||||
fn model_alias_can_be_created_without_overrides() {
|
||||
let source = test_codex_oauth_thinking_source("gpt-5.5");
|
||||
let rendered = add_model_alias_to_yaml("{}\n", &source, "gpt-5.5-alias", "").unwrap();
|
||||
let rendered = add_model_alias_to_yaml("{}\n", &source, "gpt-5.5-alias", "", false).unwrap();
|
||||
|
||||
assert!(rendered.contains("alias: gpt-5.5-alias"), "{rendered}");
|
||||
assert!(!rendered.contains("payload:"), "{rendered}");
|
||||
@@ -396,7 +408,7 @@ fn configured_model_alias_can_be_created_without_overrides() {
|
||||
.iter()
|
||||
.find(|source| source.source.model == "gpt-custom")
|
||||
.unwrap();
|
||||
let rendered = add_model_alias_to_yaml(input, source, "gpt-custom-alias", "").unwrap();
|
||||
let rendered = add_model_alias_to_yaml(input, source, "gpt-custom-alias", "", false).unwrap();
|
||||
|
||||
assert!(rendered.contains("alias: gpt-custom-alias"), "{rendered}");
|
||||
assert!(!rendered.contains("thinking:"), "{rendered}");
|
||||
@@ -414,6 +426,57 @@ fn thinking_alias_removal_cleans_legacy_combined_rules() {
|
||||
assert!(!restored.contains("service_tier: priority"), "{restored}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn model_alias_combines_reasoning_and_fast_as_independent_overrides() {
|
||||
let source = test_codex_oauth_thinking_source("gpt-5.6-sol");
|
||||
let alias = "gpt-5.6-sol-high-fast";
|
||||
let rendered = add_model_alias_to_yaml("{}\n", &source, alias, "high", true).unwrap();
|
||||
|
||||
assert!(rendered.contains("reasoning.effort: high"), "{rendered}");
|
||||
assert!(rendered.contains("service_tier: priority"), "{rendered}");
|
||||
let document: serde_norway::Value = serde_norway::from_str(&rendered).unwrap();
|
||||
let root = document.as_mapping().unwrap();
|
||||
let rules = nested_yaml_value(root, &["payload", "override"])
|
||||
.and_then(serde_norway::Value::as_sequence)
|
||||
.unwrap();
|
||||
let alias_rules = rules
|
||||
.iter()
|
||||
.filter_map(serde_norway::Value::as_mapping)
|
||||
.filter(|rule| {
|
||||
yaml_mapping_value(rule, "models")
|
||||
.and_then(serde_norway::Value::as_sequence)
|
||||
.is_some_and(|models| {
|
||||
models
|
||||
.iter()
|
||||
.any(|model| thinking_payload_model_matches(model, alias, "codex"))
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(alias_rules.len(), 2, "{rendered}");
|
||||
assert!(alias_rules.iter().any(|rule| {
|
||||
yaml_mapping_value(rule, "params")
|
||||
.and_then(serde_norway::Value::as_mapping)
|
||||
.is_some_and(|params| {
|
||||
yaml_mapping_value(params, "reasoning.effort").is_some()
|
||||
&& yaml_mapping_value(params, "service_tier").is_none()
|
||||
})
|
||||
}));
|
||||
assert!(alias_rules.iter().any(|rule| {
|
||||
yaml_mapping_value(rule, "params")
|
||||
.and_then(serde_norway::Value::as_mapping)
|
||||
.is_some_and(|params| {
|
||||
yaml_mapping_value(params, "service_tier").is_some()
|
||||
&& yaml_mapping_value(params, "reasoning.effort").is_none()
|
||||
})
|
||||
}));
|
||||
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, alias).unwrap();
|
||||
assert!(!restored.contains(alias), "{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";
|
||||
@@ -517,7 +580,7 @@ fn thinking_alias_rejects_duplicate_client_visible_name() {
|
||||
let input = "oauth-model-alias:\n codex:\n - name: gpt-5.5\n alias: gpt-5.5-high\n fork: true\n";
|
||||
let source = test_codex_oauth_thinking_source("gpt-5.4");
|
||||
assert!(
|
||||
add_model_alias_to_yaml(input, &source, "GPT-5.5-HIGH", "high")
|
||||
add_model_alias_to_yaml(input, &source, "GPT-5.5-HIGH", "high", false)
|
||||
.unwrap_err()
|
||||
.contains("已存在")
|
||||
);
|
||||
@@ -532,7 +595,8 @@ fn thinking_alias_supports_openai_compatible_model_entries() {
|
||||
.iter()
|
||||
.find(|source| source.source.model == "deepseek-chat")
|
||||
.unwrap();
|
||||
let rendered = add_model_alias_to_yaml(input, source, "deepseek-chat-high", "high").unwrap();
|
||||
let rendered =
|
||||
add_model_alias_to_yaml(input, source, "deepseek-chat-high", "high", false).unwrap();
|
||||
let value: serde_norway::Value = serde_norway::from_str(&rendered).unwrap();
|
||||
let root = value.as_mapping().unwrap();
|
||||
let providers = yaml_mapping_value(root, "openai-compatibility")
|
||||
@@ -576,7 +640,8 @@ fn thinking_alias_supports_codex_api_model_entries() {
|
||||
.iter()
|
||||
.find(|source| source.source.kind == "codex-api")
|
||||
.unwrap();
|
||||
let rendered = add_model_alias_to_yaml(input, source, "gpt-custom-xhigh", "xhigh").unwrap();
|
||||
let rendered =
|
||||
add_model_alias_to_yaml(input, source, "gpt-custom-xhigh", "xhigh", false).unwrap();
|
||||
|
||||
assert!(rendered.contains("alias: gpt-custom-xhigh"), "{rendered}");
|
||||
assert!(rendered.contains("protocol: codex"), "{rendered}");
|
||||
@@ -586,3 +651,23 @@ fn thinking_alias_supports_codex_api_model_entries() {
|
||||
assert_eq!(entries.len(), 1);
|
||||
assert_eq!(entries[0].kind, "codex-api");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn speed_alias_supports_codex_api_model_entries() {
|
||||
let input = "codex-api-key:\n - api-key: test\n base-url: https://example.com/v1\n models:\n - name: codex-custom\n";
|
||||
let available_models = test_agent_models(&["codex-custom"]);
|
||||
let sources = resolved_speed_alias_sources(input, &[], &available_models).unwrap();
|
||||
let source = sources
|
||||
.iter()
|
||||
.find(|source| source.source.kind == "codex-api")
|
||||
.unwrap();
|
||||
let rendered = add_speed_alias_to_yaml(input, source, "codex-custom-fast").unwrap();
|
||||
|
||||
assert!(rendered.contains("alias: codex-custom-fast"), "{rendered}");
|
||||
assert!(rendered.contains("protocol: codex"), "{rendered}");
|
||||
assert!(rendered.contains("service_tier: priority"), "{rendered}");
|
||||
assert!(!rendered.contains("oauth-model-alias"), "{rendered}");
|
||||
let entries = speed_aliases_from_yaml(&rendered).unwrap();
|
||||
assert_eq!(entries.len(), 1);
|
||||
assert_eq!(entries[0].kind, "codex-api");
|
||||
}
|
||||
+12
-3
@@ -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',
|
||||
|
||||
@@ -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': '高级设置子页面',
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import {
|
||||
combineModelAliasEntries,
|
||||
combineModelAliasSources,
|
||||
defaultModelAlias,
|
||||
thinkingAliasSourceKindLabel,
|
||||
@@ -7,9 +8,11 @@ import {
|
||||
} from '../src/pages/ThinkingAliasesPage';
|
||||
|
||||
describe('模型别名默认名称', () => {
|
||||
test('按思考强度生成可编辑的默认名称', () => {
|
||||
expect(defaultModelAlias('gpt-5.6-sol', 'XHigh')).toBe('gpt-5.6-sol-xhigh');
|
||||
expect(defaultModelAlias('gpt-5.6-sol', '')).toBe('gpt-5.6-sol-alias');
|
||||
test('按思考强度和 Fast 选项生成可编辑的默认名称', () => {
|
||||
expect(defaultModelAlias('gpt-5.6-sol', 'XHigh', false)).toBe('gpt-5.6-sol-xhigh');
|
||||
expect(defaultModelAlias('gpt-5.6-sol', '', true)).toBe('gpt-5.6-sol-fast');
|
||||
expect(defaultModelAlias('gpt-5.6-sol', 'xhigh', true)).toBe('gpt-5.6-sol-xhigh-fast');
|
||||
expect(defaultModelAlias('gpt-5.6-sol', '', false)).toBe('gpt-5.6-sol-alias');
|
||||
});
|
||||
|
||||
test('默认别名与现有模型重名时递增数字后缀', () => {
|
||||
@@ -22,6 +25,38 @@ describe('模型别名默认名称', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('统一模型别名列表', () => {
|
||||
test('同一个别名可同时显示思考强度和 Fast', () => {
|
||||
const identity = {
|
||||
sourceModel: 'gpt-5.6-sol',
|
||||
alias: 'gpt-5.6-sol-xhigh-fast',
|
||||
provider: 'Codex OAuth',
|
||||
kind: 'codex-oauth',
|
||||
oauthChannel: 'codex',
|
||||
};
|
||||
const entries = combineModelAliasEntries(
|
||||
[{ ...identity, effort: 'xhigh' }],
|
||||
[{ ...identity, serviceTier: 'priority' }],
|
||||
);
|
||||
|
||||
expect(entries).toHaveLength(1);
|
||||
expect(entries[0]).toMatchObject({ effort: 'xhigh', serviceTier: 'priority' });
|
||||
});
|
||||
|
||||
test('Fast-only 别名仍会显示为独立条目', () => {
|
||||
const entries = combineModelAliasEntries([], [{
|
||||
sourceModel: 'gpt-5.6-sol',
|
||||
alias: 'gpt-5.6-sol-fast',
|
||||
serviceTier: 'priority',
|
||||
provider: 'Codex OAuth',
|
||||
kind: 'codex-oauth',
|
||||
oauthChannel: 'codex',
|
||||
}]);
|
||||
|
||||
expect(entries[0]).toMatchObject({ effort: null, serviceTier: 'priority' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('思考别名来源', () => {
|
||||
test('区分同名模型的接入来源', () => {
|
||||
expect(thinkingAliasSourceKindLabel('codex-oauth')).toBe('Codex OAuth');
|
||||
@@ -44,13 +79,16 @@ describe('思考别名来源', () => {
|
||||
{ ...baseSource, id: 'plain', model: 'plain-model' },
|
||||
],
|
||||
[{ ...baseSource, id: 'reasoning', reasoningLevels: ['low', 'high'] }],
|
||||
[{ ...baseSource, id: 'reasoning', reasoningLevels: ['low', 'high'] }],
|
||||
);
|
||||
|
||||
expect(sources).toHaveLength(2);
|
||||
expect(sources.find((source) => source.id === 'reasoning')).toMatchObject({
|
||||
supportsReasoning: true,
|
||||
supportsFast: true,
|
||||
reasoningLevels: ['low', 'high'],
|
||||
});
|
||||
expect(sources.find((source) => source.id === 'plain')?.supportsReasoning).toBe(false);
|
||||
expect(sources.find((source) => source.id === 'plain')?.supportsFast).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user