refactor(models): unify preset names

This commit is contained in:
Xubin Ren
2026-08-16 11:50:56 +08:00
parent 8dc08853e4
commit 3dc38f6396
37 changed files with 239 additions and 209 deletions
+6 -4
View File
@@ -79,7 +79,9 @@ def normalize_preset_name(name: str | None, presets: dict[str, ModelPresetConfig
if not isinstance(name, str) or not name.strip():
raise ValueError("model_preset must be a non-empty string")
name = name.strip()
if name not in presets:
raise KeyError(f"model_preset {name!r} not found. Available: {', '.join(presets) or '(none)'}")
return name
if name in presets:
return name
matches = [candidate for candidate in presets if candidate.casefold() == name.casefold()]
if len(matches) == 1:
return matches[0]
raise KeyError(f"model_preset {name!r} not found. Available: {', '.join(presets) or '(none)'}")
-1
View File
@@ -235,7 +235,6 @@ def _snapshot_model_presets(
) -> dict[str, dict[str, object]]:
return {
name: {
"label": preset.label,
"model": preset.model,
"provider": preset.provider,
"max_tokens": preset.max_tokens,
@@ -3300,7 +3300,7 @@ async def test_settings_api_returns_safe_subset_and_updates_whitelist(
created_presets = {
preset["name"]: preset for preset in created_body["model_presets"]
}
assert created_presets["fast-writing"]["label"] == "Fast writing"
assert created_presets["fast-writing"]["label"] == "fast-writing"
assert created_presets["fast-writing"]["provider"] == "openai"
updated_preset = await _webui_mutate(
@@ -3320,7 +3320,7 @@ async def test_settings_api_returns_safe_subset_and_updates_whitelist(
updated_presets = {
preset["name"]: preset for preset in updated_preset_body["model_presets"]
}
assert updated_presets["fast-writing"]["label"] == "Codex"
assert updated_presets["fast-writing"]["label"] == "fast-writing"
call_order_updated = await _webui_mutate(
webui_client,
@@ -3437,7 +3437,6 @@ async def test_settings_api_returns_safe_subset_and_updates_whitelist(
assert saved.agents.defaults.provider == "atomic_chat"
assert saved.agents.defaults.model_preset == "fast-writing"
assert saved.agents.defaults.fallback_models == ["deep"]
assert saved.model_presets["fast-writing"].label == "Codex"
assert saved.model_presets["fast-writing"].model == "openai/gpt-5.5"
assert saved.model_presets["fast-writing"].provider == "openai"
assert saved.agents.defaults.timezone == "Asia/Shanghai"
-1
View File
@@ -1594,7 +1594,6 @@ def _pause(message: str = "Press Enter to continue...") -> None:
def _set_primary_quick_start_preset(config: Config, provider_name: str, model: str) -> None:
"""Store the primary preset used by Quick Start."""
config.model_presets["primary"] = ModelPresetConfig(
label="Primary",
model=model,
provider=provider_name,
)
+1 -10
View File
@@ -375,16 +375,7 @@ async def cmd_model(ctx: CommandContext) -> OutboundMessage:
metadata=metadata,
)
parts = args.split()
if len(parts) != 1:
return OutboundMessage(
channel=ctx.msg.channel,
chat_id=ctx.msg.chat_id,
content="Usage: `/model [preset]`",
metadata=metadata,
)
name = parts[0]
name = args
try:
runtime = loop.set_session_model_preset(ctx.key, name)
except (KeyError, ValueError) as exc:
+12 -2
View File
@@ -97,7 +97,6 @@ FallbackCandidate = str | InlineFallbackConfig
class ModelPresetConfig(Base):
"""A named set of model + generation parameters for quick switching."""
label: str | None = None
model: str
provider: str = "auto"
max_tokens: int = 8192
@@ -463,7 +462,18 @@ class Config(BaseSettings):
@model_validator(mode="after")
def _validate_model_preset(self) -> "Config":
if "default" in self.model_presets:
names_by_case: dict[str, str] = {}
for preset_name in self.model_presets:
if preset_name != preset_name.strip() or not preset_name.isprintable():
raise ValueError(f"invalid model_preset name {preset_name!r}")
normalized = preset_name.casefold()
if normalized in names_by_case:
raise ValueError(
"model_preset names must be unique ignoring case: "
f"{names_by_case[normalized]!r} and {preset_name!r}"
)
names_by_case[normalized] = preset_name
if "default" in names_by_case:
raise ValueError("model_preset name 'default' is reserved for agents.defaults")
name = self.agents.defaults.model_preset
if name and name != "default" and name not in self.model_presets:
+33 -20
View File
@@ -778,6 +778,25 @@ def _model_configuration_slug(label: str) -> str:
return normalized
def _model_configuration_name(value: str) -> str:
"""Validate a user-facing preset name without inventing a second identity."""
name = value.strip()
if not name:
raise WebUISettingsError("configuration name is required")
if name.casefold() == "default":
raise WebUISettingsError("configuration name is reserved")
if len(name) > 48:
raise WebUISettingsError("configuration name must be 48 characters or fewer")
if not name.isprintable():
raise WebUISettingsError("configuration name contains unsupported characters")
return name
def _model_configuration_name_exists(config: Config, name: str) -> bool:
normalized = name.casefold()
return any(existing.casefold() == normalized for existing in config.model_presets)
def _custom_provider_key(config: Config, display_name: str) -> str:
slug = _MODEL_CONFIGURATION_SLUG_RE.sub("-", display_name.strip().lower()).strip("-_")
base = f"custom-{slug or 'provider'}"
@@ -824,7 +843,7 @@ def _unique_model_configuration_name(config: Config, label: str) -> str:
base = "model"
candidate = base
suffix = 2
while candidate in config.model_presets:
while _model_configuration_name_exists(config, candidate):
candidate = f"{base}-{suffix}"
suffix += 1
return candidate
@@ -928,6 +947,8 @@ def model_settings_payload(
model_presets = [
{
"name": "default",
# Kept on the wire for older WebUI clients. It is no longer a
# separate product concept and always mirrors the canonical name.
"label": "Default",
"active": active_preset_name == "default",
"is_default": True,
@@ -958,7 +979,7 @@ def model_settings_payload(
model_presets.append(
{
"name": name,
"label": preset.label or name,
"label": name,
"active": active_preset_name == name,
"is_default": False,
"model": preset.model,
@@ -1052,20 +1073,24 @@ def create_model_configuration(
*,
oauth_status: OAuthStatusReader,
) -> str:
label = (query_first_alias(query, "label", "displayName") or "").strip()
raw_name = (query_first(query, "name") or label).strip()
raw_name = query_first(query, "name")
legacy_label = query_first_alias(query, "label", "displayName")
model = (query_first(query, "model") or "").strip()
provider = (query_first(query, "provider") or "").strip()
if not label:
label = raw_name
if not model:
raise WebUISettingsError("model is required")
if not provider:
raise WebUISettingsError("provider is required")
name = _model_configuration_slug(raw_name or label)
if name in config.model_presets:
# Old clients only sent `label`; preserve their slugging behaviour while
# new clients provide the one canonical, user-visible name directly.
name = (
_model_configuration_name(raw_name)
if raw_name is not None
else _model_configuration_slug(legacy_label or "")
)
if _model_configuration_name_exists(config, name):
raise WebUISettingsError("configuration already exists", status=409)
_validate_configured_provider(config, provider, oauth_status)
@@ -1085,7 +1110,6 @@ def create_model_configuration(
query_first_alias(query, "reasoning_effort", "reasoningEffort") or ""
).strip() or None
config.model_presets[name] = ModelPresetConfig(
label=label,
model=model,
provider=provider,
max_tokens=max_tokens if max_tokens is not None else base.max_tokens,
@@ -1115,15 +1139,6 @@ def update_model_configuration(
raise WebUISettingsError("unknown model configuration")
changed = False
label = query_first_alias(query, "label", "displayName")
if label is not None:
label = label.strip()
if not label:
raise WebUISettingsError("label is required")
if preset.label != label:
preset.label = label
changed = True
model = query_first(query, "model")
if model is not None:
model = model.strip()
@@ -1228,7 +1243,6 @@ def migrate_model_configurations(config: Config) -> bool:
label = _model_configuration_label(primary.model)
name = _unique_model_configuration_name(config, label)
config.model_presets[name] = ModelPresetConfig(
label=label,
model=primary.model,
provider=primary.provider,
max_tokens=primary.max_tokens,
@@ -1247,7 +1261,6 @@ def migrate_model_configurations(config: Config) -> bool:
label = _model_configuration_label(fallback.model)
name = _unique_model_configuration_name(config, label)
config.model_presets[name] = ModelPresetConfig(
label=label,
model=fallback.model,
provider=fallback.provider,
max_tokens=(