mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-31 08:13:11 +03:00
refactor(models): unify preset names
This commit is contained in:
@@ -47,7 +47,7 @@ Use `/model` to inspect the current runtime model:
|
|||||||
/model
|
/model
|
||||||
```
|
```
|
||||||
|
|
||||||
The response shows the current session's model and preset, plus the available preset names. Named presets come from the top-level `modelPresets` config and are the recommended way to configure model choices. `default` is always available and represents the model settings from direct `agents.defaults.*` fields.
|
The response shows the current session's model and preset, plus the available preset names. Each key under the top-level `modelPresets` config is the preset's canonical name everywhere nanobot displays or references it. `default` is always available and represents the model settings from direct `agents.defaults.*` fields.
|
||||||
|
|
||||||
To switch presets for future turns:
|
To switch presets for future turns:
|
||||||
|
|
||||||
|
|||||||
+9
-19
@@ -1404,21 +1404,6 @@ Existing configs do not need to change. Direct `agents.defaults.model`, `provide
|
|||||||
{
|
{
|
||||||
"modelPresets": {
|
"modelPresets": {
|
||||||
"fast": {
|
"fast": {
|
||||||
"provider": "openrouter",
|
|
||||||
"model": "anthropic/claude-sonnet-4.5",
|
|
||||||
"maxTokens": 4096,
|
|
||||||
"contextWindowTokens": 65536
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"agents": {
|
|
||||||
"defaults": {
|
|
||||||
"modelPreset": "fast",
|
|
||||||
"fallbackModels": ["deep", "localSmall"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"modelPresets": {
|
|
||||||
"fast": {
|
|
||||||
"label": "Fast",
|
|
||||||
"model": "gpt-4.1-mini",
|
"model": "gpt-4.1-mini",
|
||||||
"provider": "openai",
|
"provider": "openai",
|
||||||
"maxTokens": 4096,
|
"maxTokens": 4096,
|
||||||
@@ -1427,7 +1412,6 @@ Existing configs do not need to change. Direct `agents.defaults.model`, `provide
|
|||||||
"reasoningEffort": "low"
|
"reasoningEffort": "low"
|
||||||
},
|
},
|
||||||
"deep": {
|
"deep": {
|
||||||
"label": "Deep",
|
|
||||||
"model": "claude-opus-4-5",
|
"model": "claude-opus-4-5",
|
||||||
"provider": "anthropic",
|
"provider": "anthropic",
|
||||||
"maxTokens": 8192,
|
"maxTokens": 8192,
|
||||||
@@ -1435,22 +1419,28 @@ Existing configs do not need to change. Direct `agents.defaults.model`, `provide
|
|||||||
"reasoningEffort": "high"
|
"reasoningEffort": "high"
|
||||||
},
|
},
|
||||||
"localSmall": {
|
"localSmall": {
|
||||||
"label": "Local Small",
|
|
||||||
"model": "llama3.2",
|
"model": "llama3.2",
|
||||||
"provider": "ollama",
|
"provider": "ollama",
|
||||||
"maxTokens": 4096,
|
"maxTokens": 4096,
|
||||||
"contextWindowTokens": 32768,
|
"contextWindowTokens": 32768,
|
||||||
"temperature": 0.2
|
"temperature": 0.2
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"agents": {
|
||||||
|
"defaults": {
|
||||||
|
"modelPreset": "fast",
|
||||||
|
"fallbackModels": ["deep", "localSmall"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`modelPresets` is a top-level object. The keys under it (`fast`, `deep`, `coding`, etc.) are user-defined preset names. Each preset supports:
|
`modelPresets` is a top-level object. Each key (`fast`, `deep`, `coding`, etc.) is the preset's one canonical name: it is shown in the interface, passed to `/model <name>`, and referenced by defaults, fallbacks, sessions, and Dream. Names must be unique ignoring case. Each preset supports:
|
||||||
|
|
||||||
|
Older configs may still contain a `label` inside a preset. It is accepted when loading for compatibility but ignored; the object key remains the canonical name.
|
||||||
|
|
||||||
| Field | Description |
|
| Field | Description |
|
||||||
|-------|-------------|
|
|-------|-------------|
|
||||||
| `label` | Optional display name shown in model lists. |
|
|
||||||
| `model` | Model name to use for this preset. |
|
| `model` | Model name to use for this preset. |
|
||||||
| `provider` | Provider name, or `"auto"` to use provider auto-detection. |
|
| `provider` | Provider name, or `"auto"` to use provider auto-detection. |
|
||||||
| `maxTokens` | Maximum completion/output tokens. |
|
| `maxTokens` | Maximum completion/output tokens. |
|
||||||
|
|||||||
@@ -71,7 +71,6 @@ This recipe applies when one API key routes many hosted model families.
|
|||||||
},
|
},
|
||||||
"modelPresets": {
|
"modelPresets": {
|
||||||
"primary": {
|
"primary": {
|
||||||
"label": "Primary",
|
|
||||||
"provider": "openrouter",
|
"provider": "openrouter",
|
||||||
"model": "anthropic/claude-sonnet-4.5",
|
"model": "anthropic/claude-sonnet-4.5",
|
||||||
"maxTokens": 4096,
|
"maxTokens": 4096,
|
||||||
@@ -113,7 +112,6 @@ OpenCode Zen:
|
|||||||
},
|
},
|
||||||
"modelPresets": {
|
"modelPresets": {
|
||||||
"primary": {
|
"primary": {
|
||||||
"label": "OpenCode Zen",
|
|
||||||
"provider": "opencode_zen",
|
"provider": "opencode_zen",
|
||||||
"model": "opencode/deepseek-v4-pro",
|
"model": "opencode/deepseek-v4-pro",
|
||||||
"maxTokens": 4096,
|
"maxTokens": 4096,
|
||||||
@@ -140,7 +138,6 @@ OpenCode Go:
|
|||||||
},
|
},
|
||||||
"modelPresets": {
|
"modelPresets": {
|
||||||
"primary": {
|
"primary": {
|
||||||
"label": "OpenCode Go",
|
|
||||||
"provider": "opencode_go",
|
"provider": "opencode_go",
|
||||||
"model": "opencode-go/deepseek-v4-flash",
|
"model": "opencode-go/deepseek-v4-flash",
|
||||||
"maxTokens": 4096,
|
"maxTokens": 4096,
|
||||||
@@ -182,7 +179,6 @@ This recipe applies when you have an OpenAI API key and want to call OpenAI dire
|
|||||||
},
|
},
|
||||||
"modelPresets": {
|
"modelPresets": {
|
||||||
"primary": {
|
"primary": {
|
||||||
"label": "OpenAI",
|
|
||||||
"provider": "openai",
|
"provider": "openai",
|
||||||
"model": "gpt-5",
|
"model": "gpt-5",
|
||||||
"maxTokens": 4096,
|
"maxTokens": 4096,
|
||||||
@@ -219,7 +215,6 @@ This recipe applies when your key comes from Anthropic and your model name is an
|
|||||||
},
|
},
|
||||||
"modelPresets": {
|
"modelPresets": {
|
||||||
"primary": {
|
"primary": {
|
||||||
"label": "Anthropic",
|
|
||||||
"provider": "anthropic",
|
"provider": "anthropic",
|
||||||
"model": "claude-sonnet-4-5",
|
"model": "claude-sonnet-4-5",
|
||||||
"maxTokens": 4096,
|
"maxTokens": 4096,
|
||||||
@@ -255,7 +250,6 @@ If you use an Anthropic-compatible proxy, keep the preset provider as `anthropic
|
|||||||
},
|
},
|
||||||
"modelPresets": {
|
"modelPresets": {
|
||||||
"primary": {
|
"primary": {
|
||||||
"label": "Anthropic proxy",
|
|
||||||
"provider": "anthropic",
|
"provider": "anthropic",
|
||||||
"model": "claude-sonnet-4-5",
|
"model": "claude-sonnet-4-5",
|
||||||
"maxTokens": 4096,
|
"maxTokens": 4096,
|
||||||
@@ -286,7 +280,6 @@ This recipe applies when your key comes from Kimi's Coding Plan endpoint. Nanobo
|
|||||||
},
|
},
|
||||||
"modelPresets": {
|
"modelPresets": {
|
||||||
"kimiCoding": {
|
"kimiCoding": {
|
||||||
"label": "Kimi Coding",
|
|
||||||
"provider": "kimi_coding",
|
"provider": "kimi_coding",
|
||||||
"model": "kimi-for-coding",
|
"model": "kimi-for-coding",
|
||||||
"maxTokens": 4096,
|
"maxTokens": 4096,
|
||||||
@@ -324,7 +317,6 @@ This recipe applies to an OpenAI-compatible service that is not a named nanobot
|
|||||||
},
|
},
|
||||||
"modelPresets": {
|
"modelPresets": {
|
||||||
"primary": {
|
"primary": {
|
||||||
"label": "Custom",
|
|
||||||
"provider": "custom",
|
"provider": "custom",
|
||||||
"model": "provider-model-name",
|
"model": "provider-model-name",
|
||||||
"maxTokens": 4096,
|
"maxTokens": 4096,
|
||||||
@@ -364,7 +356,6 @@ For multiple custom endpoints, do not overload the single `custom` block. Name e
|
|||||||
},
|
},
|
||||||
"modelPresets": {
|
"modelPresets": {
|
||||||
"work": {
|
"work": {
|
||||||
"label": "Work proxy",
|
|
||||||
"provider": "workProxy",
|
"provider": "workProxy",
|
||||||
"model": "gpt-4o-mini",
|
"model": "gpt-4o-mini",
|
||||||
"maxTokens": 4096,
|
"maxTokens": 4096,
|
||||||
@@ -372,7 +363,6 @@ For multiple custom endpoints, do not overload the single `custom` block. Name e
|
|||||||
"temperature": 0.1
|
"temperature": 0.1
|
||||||
},
|
},
|
||||||
"lab": {
|
"lab": {
|
||||||
"label": "Lab local",
|
|
||||||
"provider": "lab-local",
|
"provider": "lab-local",
|
||||||
"model": "served-model-name",
|
"model": "served-model-name",
|
||||||
"maxTokens": 4096,
|
"maxTokens": 4096,
|
||||||
@@ -408,7 +398,6 @@ ollama pull llama3.2
|
|||||||
},
|
},
|
||||||
"modelPresets": {
|
"modelPresets": {
|
||||||
"local": {
|
"local": {
|
||||||
"label": "Local",
|
|
||||||
"provider": "ollama",
|
"provider": "ollama",
|
||||||
"model": "llama3.2",
|
"model": "llama3.2",
|
||||||
"maxTokens": 2048,
|
"maxTokens": 2048,
|
||||||
@@ -453,7 +442,6 @@ This recipe applies when a local server exposes an OpenAI-compatible `/v1` API.
|
|||||||
},
|
},
|
||||||
"modelPresets": {
|
"modelPresets": {
|
||||||
"local": {
|
"local": {
|
||||||
"label": "Local",
|
|
||||||
"provider": "vllm",
|
"provider": "vllm",
|
||||||
"model": "served-model-name",
|
"model": "served-model-name",
|
||||||
"maxTokens": 4096,
|
"maxTokens": 4096,
|
||||||
@@ -480,7 +468,6 @@ For LM Studio, use its local base URL and provider name:
|
|||||||
},
|
},
|
||||||
"modelPresets": {
|
"modelPresets": {
|
||||||
"local": {
|
"local": {
|
||||||
"label": "LM Studio",
|
|
||||||
"provider": "lm_studio",
|
"provider": "lm_studio",
|
||||||
"model": "local-model",
|
"model": "local-model",
|
||||||
"maxTokens": 2048,
|
"maxTokens": 2048,
|
||||||
@@ -505,7 +492,6 @@ This recipe applies when one provider sometimes rate-limits, one model is expens
|
|||||||
{
|
{
|
||||||
"modelPresets": {
|
"modelPresets": {
|
||||||
"fast": {
|
"fast": {
|
||||||
"label": "Fast",
|
|
||||||
"provider": "openrouter",
|
"provider": "openrouter",
|
||||||
"model": "anthropic/claude-sonnet-4.5",
|
"model": "anthropic/claude-sonnet-4.5",
|
||||||
"maxTokens": 4096,
|
"maxTokens": 4096,
|
||||||
@@ -513,7 +499,6 @@ This recipe applies when one provider sometimes rate-limits, one model is expens
|
|||||||
"temperature": 0.1
|
"temperature": 0.1
|
||||||
},
|
},
|
||||||
"deep": {
|
"deep": {
|
||||||
"label": "Deep",
|
|
||||||
"provider": "anthropic",
|
"provider": "anthropic",
|
||||||
"model": "claude-sonnet-4-5",
|
"model": "claude-sonnet-4-5",
|
||||||
"maxTokens": 4096,
|
"maxTokens": 4096,
|
||||||
@@ -521,7 +506,6 @@ This recipe applies when one provider sometimes rate-limits, one model is expens
|
|||||||
"temperature": 0.1
|
"temperature": 0.1
|
||||||
},
|
},
|
||||||
"local": {
|
"local": {
|
||||||
"label": "Local",
|
|
||||||
"provider": "ollama",
|
"provider": "ollama",
|
||||||
"model": "llama3.2",
|
"model": "llama3.2",
|
||||||
"maxTokens": 2048,
|
"maxTokens": 2048,
|
||||||
@@ -580,14 +564,12 @@ Use this after you have more than one preset and are chatting through a supporte
|
|||||||
{
|
{
|
||||||
"modelPresets": {
|
"modelPresets": {
|
||||||
"fast": {
|
"fast": {
|
||||||
"label": "Fast",
|
|
||||||
"provider": "openrouter",
|
"provider": "openrouter",
|
||||||
"model": "anthropic/claude-sonnet-4.5",
|
"model": "anthropic/claude-sonnet-4.5",
|
||||||
"maxTokens": 4096,
|
"maxTokens": 4096,
|
||||||
"contextWindowTokens": 65536
|
"contextWindowTokens": 65536
|
||||||
},
|
},
|
||||||
"local": {
|
"local": {
|
||||||
"label": "Local",
|
|
||||||
"provider": "ollama",
|
"provider": "ollama",
|
||||||
"model": "llama3.2",
|
"model": "llama3.2",
|
||||||
"maxTokens": 2048,
|
"maxTokens": 2048,
|
||||||
|
|||||||
@@ -633,7 +633,6 @@ Model presets are the recommended model configuration surface. Use them when you
|
|||||||
{
|
{
|
||||||
"modelPresets": {
|
"modelPresets": {
|
||||||
"fast": {
|
"fast": {
|
||||||
"label": "Fast",
|
|
||||||
"provider": "openrouter",
|
"provider": "openrouter",
|
||||||
"model": "anthropic/claude-sonnet-4.5",
|
"model": "anthropic/claude-sonnet-4.5",
|
||||||
"maxTokens": 4096,
|
"maxTokens": 4096,
|
||||||
@@ -641,7 +640,6 @@ Model presets are the recommended model configuration surface. Use them when you
|
|||||||
"temperature": 0.1
|
"temperature": 0.1
|
||||||
},
|
},
|
||||||
"deep": {
|
"deep": {
|
||||||
"label": "Deep",
|
|
||||||
"provider": "anthropic",
|
"provider": "anthropic",
|
||||||
"model": "claude-opus-4-5",
|
"model": "claude-opus-4-5",
|
||||||
"maxTokens": 8192,
|
"maxTokens": 8192,
|
||||||
@@ -667,7 +665,6 @@ Fallbacks are useful for transient provider failures, rate limits, or model avai
|
|||||||
{
|
{
|
||||||
"modelPresets": {
|
"modelPresets": {
|
||||||
"fast": {
|
"fast": {
|
||||||
"label": "Fast",
|
|
||||||
"provider": "openrouter",
|
"provider": "openrouter",
|
||||||
"model": "anthropic/claude-sonnet-4.5",
|
"model": "anthropic/claude-sonnet-4.5",
|
||||||
"maxTokens": 4096,
|
"maxTokens": 4096,
|
||||||
@@ -675,7 +672,6 @@ Fallbacks are useful for transient provider failures, rate limits, or model avai
|
|||||||
"temperature": 0.1
|
"temperature": 0.1
|
||||||
},
|
},
|
||||||
"deep": {
|
"deep": {
|
||||||
"label": "Deep",
|
|
||||||
"provider": "anthropic",
|
"provider": "anthropic",
|
||||||
"model": "claude-opus-4-5",
|
"model": "claude-opus-4-5",
|
||||||
"maxTokens": 8192,
|
"maxTokens": 8192,
|
||||||
@@ -683,7 +679,6 @@ Fallbacks are useful for transient provider failures, rate limits, or model avai
|
|||||||
"temperature": 0.1
|
"temperature": 0.1
|
||||||
},
|
},
|
||||||
"localSmall": {
|
"localSmall": {
|
||||||
"label": "Local Small",
|
|
||||||
"provider": "ollama",
|
"provider": "ollama",
|
||||||
"model": "llama3.2",
|
"model": "llama3.2",
|
||||||
"maxTokens": 4096,
|
"maxTokens": 4096,
|
||||||
|
|||||||
@@ -79,7 +79,9 @@ def normalize_preset_name(name: str | None, presets: dict[str, ModelPresetConfig
|
|||||||
if not isinstance(name, str) or not name.strip():
|
if not isinstance(name, str) or not name.strip():
|
||||||
raise ValueError("model_preset must be a non-empty string")
|
raise ValueError("model_preset must be a non-empty string")
|
||||||
name = name.strip()
|
name = name.strip()
|
||||||
if name not in presets:
|
if name in presets:
|
||||||
raise KeyError(f"model_preset {name!r} not found. Available: {', '.join(presets) or '(none)'}")
|
return name
|
||||||
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)'}")
|
||||||
|
|||||||
@@ -235,7 +235,6 @@ def _snapshot_model_presets(
|
|||||||
) -> dict[str, dict[str, object]]:
|
) -> dict[str, dict[str, object]]:
|
||||||
return {
|
return {
|
||||||
name: {
|
name: {
|
||||||
"label": preset.label,
|
|
||||||
"model": preset.model,
|
"model": preset.model,
|
||||||
"provider": preset.provider,
|
"provider": preset.provider,
|
||||||
"max_tokens": preset.max_tokens,
|
"max_tokens": preset.max_tokens,
|
||||||
|
|||||||
@@ -3300,7 +3300,7 @@ async def test_settings_api_returns_safe_subset_and_updates_whitelist(
|
|||||||
created_presets = {
|
created_presets = {
|
||||||
preset["name"]: preset for preset in created_body["model_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"
|
assert created_presets["fast-writing"]["provider"] == "openai"
|
||||||
|
|
||||||
updated_preset = await _webui_mutate(
|
updated_preset = await _webui_mutate(
|
||||||
@@ -3320,7 +3320,7 @@ async def test_settings_api_returns_safe_subset_and_updates_whitelist(
|
|||||||
updated_presets = {
|
updated_presets = {
|
||||||
preset["name"]: preset for preset in updated_preset_body["model_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(
|
call_order_updated = await _webui_mutate(
|
||||||
webui_client,
|
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.provider == "atomic_chat"
|
||||||
assert saved.agents.defaults.model_preset == "fast-writing"
|
assert saved.agents.defaults.model_preset == "fast-writing"
|
||||||
assert saved.agents.defaults.fallback_models == ["deep"]
|
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"].model == "openai/gpt-5.5"
|
||||||
assert saved.model_presets["fast-writing"].provider == "openai"
|
assert saved.model_presets["fast-writing"].provider == "openai"
|
||||||
assert saved.agents.defaults.timezone == "Asia/Shanghai"
|
assert saved.agents.defaults.timezone == "Asia/Shanghai"
|
||||||
|
|||||||
@@ -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:
|
def _set_primary_quick_start_preset(config: Config, provider_name: str, model: str) -> None:
|
||||||
"""Store the primary preset used by Quick Start."""
|
"""Store the primary preset used by Quick Start."""
|
||||||
config.model_presets["primary"] = ModelPresetConfig(
|
config.model_presets["primary"] = ModelPresetConfig(
|
||||||
label="Primary",
|
|
||||||
model=model,
|
model=model,
|
||||||
provider=provider_name,
|
provider=provider_name,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -375,16 +375,7 @@ async def cmd_model(ctx: CommandContext) -> OutboundMessage:
|
|||||||
metadata=metadata,
|
metadata=metadata,
|
||||||
)
|
)
|
||||||
|
|
||||||
parts = args.split()
|
name = args
|
||||||
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]
|
|
||||||
try:
|
try:
|
||||||
runtime = loop.set_session_model_preset(ctx.key, name)
|
runtime = loop.set_session_model_preset(ctx.key, name)
|
||||||
except (KeyError, ValueError) as exc:
|
except (KeyError, ValueError) as exc:
|
||||||
|
|||||||
@@ -97,7 +97,6 @@ FallbackCandidate = str | InlineFallbackConfig
|
|||||||
class ModelPresetConfig(Base):
|
class ModelPresetConfig(Base):
|
||||||
"""A named set of model + generation parameters for quick switching."""
|
"""A named set of model + generation parameters for quick switching."""
|
||||||
|
|
||||||
label: str | None = None
|
|
||||||
model: str
|
model: str
|
||||||
provider: str = "auto"
|
provider: str = "auto"
|
||||||
max_tokens: int = 8192
|
max_tokens: int = 8192
|
||||||
@@ -463,7 +462,18 @@ class Config(BaseSettings):
|
|||||||
|
|
||||||
@model_validator(mode="after")
|
@model_validator(mode="after")
|
||||||
def _validate_model_preset(self) -> "Config":
|
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")
|
raise ValueError("model_preset name 'default' is reserved for agents.defaults")
|
||||||
name = self.agents.defaults.model_preset
|
name = self.agents.defaults.model_preset
|
||||||
if name and name != "default" and name not in self.model_presets:
|
if name and name != "default" and name not in self.model_presets:
|
||||||
|
|||||||
@@ -778,6 +778,25 @@ def _model_configuration_slug(label: str) -> str:
|
|||||||
return normalized
|
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:
|
def _custom_provider_key(config: Config, display_name: str) -> str:
|
||||||
slug = _MODEL_CONFIGURATION_SLUG_RE.sub("-", display_name.strip().lower()).strip("-_")
|
slug = _MODEL_CONFIGURATION_SLUG_RE.sub("-", display_name.strip().lower()).strip("-_")
|
||||||
base = f"custom-{slug or 'provider'}"
|
base = f"custom-{slug or 'provider'}"
|
||||||
@@ -824,7 +843,7 @@ def _unique_model_configuration_name(config: Config, label: str) -> str:
|
|||||||
base = "model"
|
base = "model"
|
||||||
candidate = base
|
candidate = base
|
||||||
suffix = 2
|
suffix = 2
|
||||||
while candidate in config.model_presets:
|
while _model_configuration_name_exists(config, candidate):
|
||||||
candidate = f"{base}-{suffix}"
|
candidate = f"{base}-{suffix}"
|
||||||
suffix += 1
|
suffix += 1
|
||||||
return candidate
|
return candidate
|
||||||
@@ -928,6 +947,8 @@ def model_settings_payload(
|
|||||||
model_presets = [
|
model_presets = [
|
||||||
{
|
{
|
||||||
"name": "default",
|
"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",
|
"label": "Default",
|
||||||
"active": active_preset_name == "default",
|
"active": active_preset_name == "default",
|
||||||
"is_default": True,
|
"is_default": True,
|
||||||
@@ -958,7 +979,7 @@ def model_settings_payload(
|
|||||||
model_presets.append(
|
model_presets.append(
|
||||||
{
|
{
|
||||||
"name": name,
|
"name": name,
|
||||||
"label": preset.label or name,
|
"label": name,
|
||||||
"active": active_preset_name == name,
|
"active": active_preset_name == name,
|
||||||
"is_default": False,
|
"is_default": False,
|
||||||
"model": preset.model,
|
"model": preset.model,
|
||||||
@@ -1052,20 +1073,24 @@ def create_model_configuration(
|
|||||||
*,
|
*,
|
||||||
oauth_status: OAuthStatusReader,
|
oauth_status: OAuthStatusReader,
|
||||||
) -> str:
|
) -> str:
|
||||||
label = (query_first_alias(query, "label", "displayName") or "").strip()
|
raw_name = query_first(query, "name")
|
||||||
raw_name = (query_first(query, "name") or label).strip()
|
legacy_label = query_first_alias(query, "label", "displayName")
|
||||||
model = (query_first(query, "model") or "").strip()
|
model = (query_first(query, "model") or "").strip()
|
||||||
provider = (query_first(query, "provider") or "").strip()
|
provider = (query_first(query, "provider") or "").strip()
|
||||||
|
|
||||||
if not label:
|
|
||||||
label = raw_name
|
|
||||||
if not model:
|
if not model:
|
||||||
raise WebUISettingsError("model is required")
|
raise WebUISettingsError("model is required")
|
||||||
if not provider:
|
if not provider:
|
||||||
raise WebUISettingsError("provider is required")
|
raise WebUISettingsError("provider is required")
|
||||||
|
|
||||||
name = _model_configuration_slug(raw_name or label)
|
# Old clients only sent `label`; preserve their slugging behaviour while
|
||||||
if name in config.model_presets:
|
# 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)
|
raise WebUISettingsError("configuration already exists", status=409)
|
||||||
_validate_configured_provider(config, provider, oauth_status)
|
_validate_configured_provider(config, provider, oauth_status)
|
||||||
|
|
||||||
@@ -1085,7 +1110,6 @@ def create_model_configuration(
|
|||||||
query_first_alias(query, "reasoning_effort", "reasoningEffort") or ""
|
query_first_alias(query, "reasoning_effort", "reasoningEffort") or ""
|
||||||
).strip() or None
|
).strip() or None
|
||||||
config.model_presets[name] = ModelPresetConfig(
|
config.model_presets[name] = ModelPresetConfig(
|
||||||
label=label,
|
|
||||||
model=model,
|
model=model,
|
||||||
provider=provider,
|
provider=provider,
|
||||||
max_tokens=max_tokens if max_tokens is not None else base.max_tokens,
|
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")
|
raise WebUISettingsError("unknown model configuration")
|
||||||
|
|
||||||
changed = False
|
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")
|
model = query_first(query, "model")
|
||||||
if model is not None:
|
if model is not None:
|
||||||
model = model.strip()
|
model = model.strip()
|
||||||
@@ -1228,7 +1243,6 @@ def migrate_model_configurations(config: Config) -> bool:
|
|||||||
label = _model_configuration_label(primary.model)
|
label = _model_configuration_label(primary.model)
|
||||||
name = _unique_model_configuration_name(config, label)
|
name = _unique_model_configuration_name(config, label)
|
||||||
config.model_presets[name] = ModelPresetConfig(
|
config.model_presets[name] = ModelPresetConfig(
|
||||||
label=label,
|
|
||||||
model=primary.model,
|
model=primary.model,
|
||||||
provider=primary.provider,
|
provider=primary.provider,
|
||||||
max_tokens=primary.max_tokens,
|
max_tokens=primary.max_tokens,
|
||||||
@@ -1247,7 +1261,6 @@ def migrate_model_configurations(config: Config) -> bool:
|
|||||||
label = _model_configuration_label(fallback.model)
|
label = _model_configuration_label(fallback.model)
|
||||||
name = _unique_model_configuration_name(config, label)
|
name = _unique_model_configuration_name(config, label)
|
||||||
config.model_presets[name] = ModelPresetConfig(
|
config.model_presets[name] = ModelPresetConfig(
|
||||||
label=label,
|
|
||||||
model=fallback.model,
|
model=fallback.model,
|
||||||
provider=fallback.provider,
|
provider=fallback.provider,
|
||||||
max_tokens=(
|
max_tokens=(
|
||||||
|
|||||||
@@ -361,3 +361,15 @@ def test_resolver_mutates_only_its_default_selection() -> None:
|
|||||||
assert resolver.model_preset is None
|
assert resolver.model_preset is None
|
||||||
assert initial.model == "base-model"
|
assert initial.model == "base-model"
|
||||||
assert initial.context_window_tokens == 10_000
|
assert initial.context_window_tokens == 10_000
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolver_preserves_canonical_preset_name_for_case_insensitive_input() -> None:
|
||||||
|
resolver = ModelRuntimeResolver(
|
||||||
|
_runtime(),
|
||||||
|
model_presets={"Deep Research": ModelPresetConfig(model="deep-model")},
|
||||||
|
)
|
||||||
|
|
||||||
|
selected = resolver.select_preset("deep research")
|
||||||
|
|
||||||
|
assert selected.model == "deep-model"
|
||||||
|
assert selected.model_preset == "Deep Research"
|
||||||
|
|||||||
@@ -33,14 +33,14 @@ def _provider(default_model: str, max_tokens: int = 123) -> MagicMock:
|
|||||||
return provider
|
return provider
|
||||||
|
|
||||||
|
|
||||||
def _make_loop(tmp_path, *, preset_snapshot_loader=None) -> AgentLoop:
|
def _make_loop(tmp_path, *, preset_snapshot_loader=None, model_presets=None) -> AgentLoop:
|
||||||
return AgentLoop(
|
return AgentLoop(
|
||||||
bus=MessageBus(),
|
bus=MessageBus(),
|
||||||
provider=_provider("base-model", max_tokens=123),
|
provider=_provider("base-model", max_tokens=123),
|
||||||
workspace=tmp_path,
|
workspace=tmp_path,
|
||||||
model="base-model",
|
model="base-model",
|
||||||
context_window_tokens=1000,
|
context_window_tokens=1000,
|
||||||
model_presets={
|
model_presets=model_presets or {
|
||||||
"default": ModelPresetConfig(
|
"default": ModelPresetConfig(
|
||||||
model="base-model",
|
model="base-model",
|
||||||
max_tokens=123,
|
max_tokens=123,
|
||||||
@@ -106,6 +106,24 @@ async def test_model_command_switches_preset(tmp_path) -> None:
|
|||||||
assert status is not None and "openai/gpt-4.1" in status.content
|
assert status is not None and "openai/gpt-4.1" in status.content
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_model_command_accepts_canonical_names_with_spaces(tmp_path) -> None:
|
||||||
|
loop = _make_loop(
|
||||||
|
tmp_path,
|
||||||
|
model_presets={
|
||||||
|
"default": ModelPresetConfig(model="base-model"),
|
||||||
|
"Deep Research": ModelPresetConfig(model="deep-model"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
out = await cmd_model(
|
||||||
|
_ctx(loop, "/model deep research", args="deep research"),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "Switched model preset to `Deep Research`." in out.content
|
||||||
|
assert _saved_model_preset(loop) == "Deep Research"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_model_command_switches_back_to_default(tmp_path) -> None:
|
async def test_model_command_switches_back_to_default(tmp_path) -> None:
|
||||||
loop = _make_loop(tmp_path)
|
loop = _make_loop(tmp_path)
|
||||||
|
|||||||
@@ -275,6 +275,30 @@ def test_model_presets_accepts_camel_case_root_key() -> None:
|
|||||||
assert config.model_presets["fast"].provider == "openai"
|
assert config.model_presets["fast"].provider == "openai"
|
||||||
|
|
||||||
|
|
||||||
|
def test_legacy_model_preset_label_is_ignored() -> None:
|
||||||
|
config = Config.model_validate({
|
||||||
|
"modelPresets": {
|
||||||
|
"gpt-5-6-sol": {
|
||||||
|
"label": "Codex",
|
||||||
|
"model": "openai-codex/gpt-5.6-luna",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
assert set(config.model_presets) == {"gpt-5-6-sol"}
|
||||||
|
assert "label" not in config.model_presets["gpt-5-6-sol"].model_dump()
|
||||||
|
|
||||||
|
|
||||||
|
def test_model_preset_names_are_unique_ignoring_case() -> None:
|
||||||
|
with pytest.raises(ValueError, match="unique ignoring case"):
|
||||||
|
Config.model_validate({
|
||||||
|
"modelPresets": {
|
||||||
|
"Fast": {"model": "openai/gpt-4.1-mini"},
|
||||||
|
"fast": {"model": "openai/gpt-4.1"},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
def test_model_presets_serializes_with_camel_case_root_key() -> None:
|
def test_model_presets_serializes_with_camel_case_root_key() -> None:
|
||||||
config = Config.model_validate({
|
config = Config.model_validate({
|
||||||
"model_presets": {
|
"model_presets": {
|
||||||
|
|||||||
@@ -236,7 +236,7 @@ def _dynamic_provider_config(
|
|||||||
return Config.model_validate(raw_config)
|
return Config.model_validate(raw_config)
|
||||||
|
|
||||||
|
|
||||||
def test_create_model_configuration_writes_label_without_changing_call_order(
|
def test_create_model_configuration_accepts_legacy_label_without_changing_call_order(
|
||||||
tmp_path,
|
tmp_path,
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -260,11 +260,10 @@ def test_create_model_configuration_writes_label_without_changing_call_order(
|
|||||||
assert payload["agent"]["model"] == "openai/gpt-4o"
|
assert payload["agent"]["model"] == "openai/gpt-4o"
|
||||||
assert payload["created_model_preset"] == "fast-writing"
|
assert payload["created_model_preset"] == "fast-writing"
|
||||||
rows = {row["name"]: row for row in payload["model_presets"]}
|
rows = {row["name"]: row for row in payload["model_presets"]}
|
||||||
assert rows["fast-writing"]["label"] == "Fast writing"
|
assert rows["fast-writing"]["label"] == "fast-writing"
|
||||||
|
|
||||||
saved = load_config(config_path)
|
saved = load_config(config_path)
|
||||||
assert saved.agents.defaults.model_preset is None
|
assert saved.agents.defaults.model_preset is None
|
||||||
assert saved.model_presets["fast-writing"].label == "Fast writing"
|
|
||||||
assert saved.model_presets["fast-writing"].model == "openai/gpt-4.1-mini"
|
assert saved.model_presets["fast-writing"].model == "openai/gpt-4.1-mini"
|
||||||
assert saved.model_presets["fast-writing"].provider == "openai"
|
assert saved.model_presets["fast-writing"].provider == "openai"
|
||||||
|
|
||||||
@@ -279,6 +278,40 @@ def test_create_model_configuration_writes_label_without_changing_call_order(
|
|||||||
assert duplicate.value.status == 409
|
assert duplicate.value.status == 409
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_model_configuration_preserves_canonical_name(
|
||||||
|
tmp_path,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
config_path = tmp_path / "config.json"
|
||||||
|
config = Config()
|
||||||
|
config.providers.openai.api_key = "sk-test"
|
||||||
|
save_config(config, config_path)
|
||||||
|
monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)
|
||||||
|
|
||||||
|
payload = create_model_configuration(
|
||||||
|
{
|
||||||
|
"name": ["Fast Writing"],
|
||||||
|
"provider": ["openai"],
|
||||||
|
"model": ["openai/gpt-4.1-mini"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert payload["created_model_preset"] == "Fast Writing"
|
||||||
|
rows = {row["name"]: row for row in payload["model_presets"]}
|
||||||
|
assert rows["Fast Writing"]["label"] == "Fast Writing"
|
||||||
|
assert "Fast Writing" in load_config(config_path).model_presets
|
||||||
|
|
||||||
|
with pytest.raises(WebUISettingsError) as duplicate:
|
||||||
|
create_model_configuration(
|
||||||
|
{
|
||||||
|
"name": ["fast writing"],
|
||||||
|
"provider": ["openai"],
|
||||||
|
"model": ["openai/gpt-4.1-mini"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
assert duplicate.value.status == 409
|
||||||
|
|
||||||
|
|
||||||
def test_create_model_configuration_accepts_dynamic_custom_provider(
|
def test_create_model_configuration_accepts_dynamic_custom_provider(
|
||||||
tmp_path,
|
tmp_path,
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
@@ -353,7 +386,6 @@ def test_update_model_configuration_edits_named_preset_without_selecting(
|
|||||||
config = Config()
|
config = Config()
|
||||||
config.providers.openai.api_key = "sk-test"
|
config.providers.openai.api_key = "sk-test"
|
||||||
config.model_presets["codex"] = ModelPresetConfig(
|
config.model_presets["codex"] = ModelPresetConfig(
|
||||||
label="Old Codex",
|
|
||||||
provider="openai",
|
provider="openai",
|
||||||
model="openai/gpt-4.1",
|
model="openai/gpt-4.1",
|
||||||
)
|
)
|
||||||
@@ -382,7 +414,6 @@ def test_update_model_configuration_edits_named_preset_without_selecting(
|
|||||||
assert payload["agent"]["model"] == "anthropic/claude-opus-4-5"
|
assert payload["agent"]["model"] == "anthropic/claude-opus-4-5"
|
||||||
saved = load_config(config_path)
|
saved = load_config(config_path)
|
||||||
assert saved.agents.defaults.model_preset is None
|
assert saved.agents.defaults.model_preset is None
|
||||||
assert saved.model_presets["codex"].label == "Codex"
|
|
||||||
assert saved.model_presets["codex"].provider == "openai_codex"
|
assert saved.model_presets["codex"].provider == "openai_codex"
|
||||||
assert saved.model_presets["codex"].model == "openai-codex/gpt-5.5"
|
assert saved.model_presets["codex"].model == "openai-codex/gpt-5.5"
|
||||||
|
|
||||||
@@ -834,7 +865,6 @@ def test_update_model_configuration_preserves_custom_context_windows(
|
|||||||
config_path = tmp_path / "config.json"
|
config_path = tmp_path / "config.json"
|
||||||
config = Config()
|
config = Config()
|
||||||
config.model_presets["codex"] = ModelPresetConfig(
|
config.model_presets["codex"] = ModelPresetConfig(
|
||||||
label="Codex",
|
|
||||||
provider="openai",
|
provider="openai",
|
||||||
model="openai/gpt-4.1",
|
model="openai/gpt-4.1",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ export interface AgentSettingsDraft {
|
|||||||
model: string;
|
model: string;
|
||||||
provider: string;
|
provider: string;
|
||||||
modelPreset: string;
|
modelPreset: string;
|
||||||
presetLabel: string;
|
|
||||||
maxTokens: number;
|
maxTokens: number;
|
||||||
contextWindowTokens: number;
|
contextWindowTokens: number;
|
||||||
temperature: number;
|
temperature: number;
|
||||||
@@ -67,7 +66,6 @@ export const DEFAULT_AGENT_SETTINGS_DRAFT: AgentSettingsDraft = {
|
|||||||
model: "",
|
model: "",
|
||||||
provider: "",
|
provider: "",
|
||||||
modelPreset: "",
|
modelPreset: "",
|
||||||
presetLabel: "",
|
|
||||||
maxTokens: 8192,
|
maxTokens: 8192,
|
||||||
contextWindowTokens: 200_000,
|
contextWindowTokens: 200_000,
|
||||||
temperature: 0.1,
|
temperature: 0.1,
|
||||||
@@ -89,7 +87,6 @@ export function agentDraftFromPayload(
|
|||||||
model: activePreset?.model ?? payload.agent.model,
|
model: activePreset?.model ?? payload.agent.model,
|
||||||
provider: activePreset?.provider ?? payload.agent.provider ?? payload.agent.resolved_provider ?? "",
|
provider: activePreset?.provider ?? payload.agent.provider ?? payload.agent.resolved_provider ?? "",
|
||||||
modelPreset: activePresetName,
|
modelPreset: activePresetName,
|
||||||
presetLabel: activePreset?.label ?? activePresetName,
|
|
||||||
maxTokens: activePreset?.max_tokens ?? payload.agent.max_tokens,
|
maxTokens: activePreset?.max_tokens ?? payload.agent.max_tokens,
|
||||||
contextWindowTokens: normalizeContextWindowTokens(
|
contextWindowTokens: normalizeContextWindowTokens(
|
||||||
activePreset?.context_window_tokens ?? payload.agent.context_window_tokens,
|
activePreset?.context_window_tokens ?? payload.agent.context_window_tokens,
|
||||||
@@ -126,7 +123,7 @@ export function ModelPresetDeleteDialog({
|
|||||||
{tx(
|
{tx(
|
||||||
"settings.models.deletePresetHelp",
|
"settings.models.deletePresetHelp",
|
||||||
"This removes the preset “{{name}}”. Provider credentials are not affected.",
|
"This removes the preset “{{name}}”. Provider credentials are not affected.",
|
||||||
{ name: preset?.label ?? "" },
|
{ name: preset?.name ?? "" },
|
||||||
)}
|
)}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
@@ -266,7 +263,7 @@ export function ModelsSettings({
|
|||||||
const modelFieldsMissing =
|
const modelFieldsMissing =
|
||||||
!form.model.trim() ||
|
!form.model.trim() ||
|
||||||
!form.provider.trim() ||
|
!form.provider.trim() ||
|
||||||
!form.presetLabel.trim() ||
|
(creating && !form.modelPreset.trim()) ||
|
||||||
form.maxTokens <= 0 ||
|
form.maxTokens <= 0 ||
|
||||||
form.temperature < 0 ||
|
form.temperature < 0 ||
|
||||||
form.temperature > 2;
|
form.temperature > 2;
|
||||||
@@ -290,7 +287,6 @@ export function ModelsSettings({
|
|||||||
modelPreset: preset.name,
|
modelPreset: preset.name,
|
||||||
model: preset.model,
|
model: preset.model,
|
||||||
provider: preset.provider,
|
provider: preset.provider,
|
||||||
presetLabel: preset.label,
|
|
||||||
maxTokens: preset.max_tokens,
|
maxTokens: preset.max_tokens,
|
||||||
contextWindowTokens: normalizeContextWindowTokens(preset.context_window_tokens),
|
contextWindowTokens: normalizeContextWindowTokens(preset.context_window_tokens),
|
||||||
temperature: preset.temperature,
|
temperature: preset.temperature,
|
||||||
@@ -351,30 +347,29 @@ export function ModelsSettings({
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<SettingsRow
|
<SettingsRow
|
||||||
title={
|
title={tx("settings.models.presetName", "Preset name")}
|
||||||
creating
|
|
||||||
? tx("settings.models.presetName", "Preset name")
|
|
||||||
: tx("settings.models.presetDisplayName", "Display name")
|
|
||||||
}
|
|
||||||
description={
|
description={
|
||||||
creating
|
creating
|
||||||
? undefined
|
? tx(
|
||||||
: tx(
|
"settings.models.presetNameHelp",
|
||||||
"settings.models.presetDisplayNameHelp",
|
"Used everywhere, including /model commands. Names must be unique.",
|
||||||
"Shown in the interface. The command name stays /model {{name}}.",
|
|
||||||
{ name: form.modelPreset },
|
|
||||||
)
|
)
|
||||||
|
: undefined
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Input
|
{creating ? (
|
||||||
autoFocus={creating}
|
<Input
|
||||||
value={form.presetLabel}
|
autoFocus
|
||||||
placeholder={tx("settings.models.presetNamePlaceholder", "Fast writing")}
|
value={form.modelPreset}
|
||||||
onChange={(event) =>
|
placeholder={tx("settings.models.presetNamePlaceholder", "Fast writing")}
|
||||||
setForm((prev) => ({ ...prev, presetLabel: event.target.value }))
|
onChange={(event) =>
|
||||||
}
|
setForm((prev) => ({ ...prev, modelPreset: event.target.value }))
|
||||||
className="h-8 w-[min(280px,70vw)] rounded-full text-[13px]"
|
}
|
||||||
/>
|
className="h-8 w-[min(280px,70vw)] rounded-full text-[13px]"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span className="text-[13px] font-medium text-foreground">{form.modelPreset}</span>
|
||||||
|
)}
|
||||||
</SettingsRow>
|
</SettingsRow>
|
||||||
<SettingsRow title={t("settings.rows.provider")}>
|
<SettingsRow title={t("settings.rows.provider")}>
|
||||||
<ProviderPicker
|
<ProviderPicker
|
||||||
@@ -604,11 +599,11 @@ export function ModelsSettings({
|
|||||||
draggable={ordered && !callOrderBusy}
|
draggable={ordered && !callOrderBusy}
|
||||||
aria-label={
|
aria-label={
|
||||||
ordered
|
ordered
|
||||||
? `${preset?.label ?? name}. ${tx(
|
? `${name}. ${tx(
|
||||||
"settings.models.dragToReorder",
|
"settings.models.dragToReorder",
|
||||||
"Drag to reorder",
|
"Drag to reorder",
|
||||||
)}`
|
)}`
|
||||||
: preset?.label ?? name
|
: name
|
||||||
}
|
}
|
||||||
data-testid={`model-call-order-row-${name}`}
|
data-testid={`model-call-order-row-${name}`}
|
||||||
onDragStart={(event) => {
|
onDragStart={(event) => {
|
||||||
@@ -704,7 +699,7 @@ export function ModelsSettings({
|
|||||||
<span className="min-w-0 flex-1">
|
<span className="min-w-0 flex-1">
|
||||||
<span className="flex min-w-0 flex-wrap items-center gap-2">
|
<span className="flex min-w-0 flex-wrap items-center gap-2">
|
||||||
<span className="truncate text-[14px] font-medium text-foreground">
|
<span className="truncate text-[14px] font-medium text-foreground">
|
||||||
{preset?.label ?? name}
|
{name}
|
||||||
</span>
|
</span>
|
||||||
{orderIndex === 0 ? (
|
{orderIndex === 0 ? (
|
||||||
<StatusPill tone="success">
|
<StatusPill tone="success">
|
||||||
|
|||||||
@@ -128,11 +128,11 @@ export function useModelSettingsActions({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (modelPresetCreating) {
|
if (modelPresetCreating) {
|
||||||
const label = form.presetLabel.trim();
|
const name = form.modelPreset.trim();
|
||||||
const provider = form.provider.trim();
|
const provider = form.provider.trim();
|
||||||
const model = form.model.trim();
|
const model = form.model.trim();
|
||||||
if (
|
if (
|
||||||
!label ||
|
!name ||
|
||||||
!provider ||
|
!provider ||
|
||||||
!model ||
|
!model ||
|
||||||
form.maxTokens <= 0 ||
|
form.maxTokens <= 0 ||
|
||||||
@@ -145,7 +145,7 @@ export function useModelSettingsActions({
|
|||||||
setModelConfigurationSaving(true);
|
setModelConfigurationSaving(true);
|
||||||
try {
|
try {
|
||||||
const payload = await createModelConfiguration(client, {
|
const payload = await createModelConfiguration(client, {
|
||||||
label,
|
name,
|
||||||
provider,
|
provider,
|
||||||
model,
|
model,
|
||||||
maxTokens: form.maxTokens,
|
maxTokens: form.maxTokens,
|
||||||
@@ -190,10 +190,6 @@ export function useModelSettingsActions({
|
|||||||
try {
|
try {
|
||||||
const payload = await updateModelConfiguration(client, {
|
const payload = await updateModelConfiguration(client, {
|
||||||
name: selectedPreset.name,
|
name: selectedPreset.name,
|
||||||
label:
|
|
||||||
form.presetLabel.trim() !== selectedPreset.label
|
|
||||||
? form.presetLabel.trim()
|
|
||||||
: undefined,
|
|
||||||
model: form.model !== selectedPreset.model ? form.model : undefined,
|
model: form.model !== selectedPreset.model ? form.model : undefined,
|
||||||
provider: form.provider !== selectedPreset.provider ? form.provider : undefined,
|
provider: form.provider !== selectedPreset.provider ? form.provider : undefined,
|
||||||
maxTokens:
|
maxTokens:
|
||||||
@@ -235,7 +231,6 @@ export function useModelSettingsActions({
|
|||||||
setForm((prev) => ({
|
setForm((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
modelPreset: "",
|
modelPreset: "",
|
||||||
presetLabel: "",
|
|
||||||
provider,
|
provider,
|
||||||
model: "",
|
model: "",
|
||||||
maxTokens: primaryPreset?.max_tokens ?? settings.agent.max_tokens,
|
maxTokens: primaryPreset?.max_tokens ?? settings.agent.max_tokens,
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export function OverviewSettings({
|
|||||||
const activePresetName = settings.agent.model_preset;
|
const activePresetName = settings.agent.model_preset;
|
||||||
const activePreset =
|
const activePreset =
|
||||||
activePresetName && activePresetName !== "default"
|
activePresetName && activePresetName !== "default"
|
||||||
? settings.model_presets.find((preset) => preset.name === activePresetName)?.label ??
|
? settings.model_presets.find((preset) => preset.name === activePresetName)?.name ??
|
||||||
activePresetName
|
activePresetName
|
||||||
: null;
|
: null;
|
||||||
const activeProvider = settings.agent.resolved_provider ?? settings.agent.provider;
|
const activeProvider = settings.agent.resolved_provider ?? settings.agent.provider;
|
||||||
|
|||||||
@@ -261,8 +261,7 @@ export function useSettingsController({
|
|||||||
form.maxTokens !== selectedPreset.max_tokens ||
|
form.maxTokens !== selectedPreset.max_tokens ||
|
||||||
form.contextWindowTokens !== normalizeContextWindowTokens(selectedPreset.context_window_tokens) ||
|
form.contextWindowTokens !== normalizeContextWindowTokens(selectedPreset.context_window_tokens) ||
|
||||||
form.temperature !== selectedPreset.temperature ||
|
form.temperature !== selectedPreset.temperature ||
|
||||||
form.reasoningEffort !== (selectedPreset.reasoning_effort ?? "") ||
|
form.reasoningEffort !== (selectedPreset.reasoning_effort ?? "")
|
||||||
form.presetLabel.trim() !== selectedPreset.label
|
|
||||||
);
|
);
|
||||||
}, [form, settings]);
|
}, [form, settings]);
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import { cn } from "@/lib/utils";
|
|||||||
|
|
||||||
export interface ModelPresetOption {
|
export interface ModelPresetOption {
|
||||||
name: string;
|
name: string;
|
||||||
label: string;
|
|
||||||
model?: string | null;
|
model?: string | null;
|
||||||
provider?: string | null;
|
provider?: string | null;
|
||||||
}
|
}
|
||||||
@@ -99,7 +98,6 @@ export function ModelPresetBadge({
|
|||||||
const activePreset: ModelPresetOption = {
|
const activePreset: ModelPresetOption = {
|
||||||
...(listedIndex >= 0 ? modelPresets[listedIndex] : undefined),
|
...(listedIndex >= 0 ? modelPresets[listedIndex] : undefined),
|
||||||
name: activeName,
|
name: activeName,
|
||||||
label: label || modelPresets[listedIndex]?.label || activeName,
|
|
||||||
model: modelDetail ?? modelPresets[listedIndex]?.model,
|
model: modelDetail ?? modelPresets[listedIndex]?.model,
|
||||||
provider: provider || modelPresets[listedIndex]?.provider,
|
provider: provider || modelPresets[listedIndex]?.provider,
|
||||||
};
|
};
|
||||||
@@ -225,7 +223,7 @@ export function ModelPresetBadge({
|
|||||||
aria-valuemax={canSwitch ? presets.length - 1 : undefined}
|
aria-valuemax={canSwitch ? presets.length - 1 : undefined}
|
||||||
aria-valuemin={canSwitch ? 0 : undefined}
|
aria-valuemin={canSwitch ? 0 : undefined}
|
||||||
aria-valuenow={canSwitch ? previewIndex : undefined}
|
aria-valuenow={canSwitch ? previewIndex : undefined}
|
||||||
aria-valuetext={canSwitch ? previewPreset?.label || label : undefined}
|
aria-valuetext={canSwitch ? previewPreset?.name || label : undefined}
|
||||||
role={canSwitch ? "spinbutton" : undefined}
|
role={canSwitch ? "spinbutton" : undefined}
|
||||||
type={interactive || canSwitch ? "button" : undefined}
|
type={interactive || canSwitch ? "button" : undefined}
|
||||||
onClick={interactive ? onClick : undefined}
|
onClick={interactive ? onClick : undefined}
|
||||||
@@ -290,7 +288,7 @@ export function ModelPresetBadge({
|
|||||||
return (
|
return (
|
||||||
<PresetPill
|
<PresetPill
|
||||||
key={virtualIndex}
|
key={virtualIndex}
|
||||||
label={preset.label || preset.name}
|
label={preset.name}
|
||||||
modelDetail={preset.model}
|
modelDetail={preset.model}
|
||||||
provider={preset.provider}
|
provider={preset.provider}
|
||||||
isHero={isHero}
|
isHero={isHero}
|
||||||
|
|||||||
@@ -378,7 +378,11 @@ function toModelBadgeInfo(
|
|||||||
const model = scopedPreset
|
const model = scopedPreset
|
||||||
? preset?.model || null
|
? preset?.model || null
|
||||||
: settings?.agent.model || modelName || null;
|
: settings?.agent.model || modelName || null;
|
||||||
const label = preset?.label?.trim() || scopedPreset || toModelBadgeLabel(model);
|
const label = preset
|
||||||
|
? preset.is_default
|
||||||
|
? preset.label?.trim() || "Default"
|
||||||
|
: preset.name.trim()
|
||||||
|
: scopedPreset || toModelBadgeLabel(model);
|
||||||
const rawProvider = preset?.provider
|
const rawProvider = preset?.provider
|
||||||
|| (!scopedPreset ? settings?.agent.provider : null)
|
|| (!scopedPreset ? settings?.agent.provider : null)
|
||||||
|| null;
|
|| null;
|
||||||
@@ -419,7 +423,6 @@ function modelPresetOptionsFromSettings(
|
|||||||
const name = preset.name.trim();
|
const name = preset.name.trim();
|
||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
label: preset.label?.trim() || name,
|
|
||||||
model: preset.model,
|
model: preset.model,
|
||||||
provider: preset.resolved_provider || preset.provider,
|
provider: preset.resolved_provider || preset.provider,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -143,9 +143,7 @@
|
|||||||
"presets": "Model presets",
|
"presets": "Model presets",
|
||||||
"editPreset": "Edit preset",
|
"editPreset": "Edit preset",
|
||||||
"presetName": "Preset name",
|
"presetName": "Preset name",
|
||||||
"presetNameHelp": "A short name used throughout model settings.",
|
"presetNameHelp": "Used in the interface and /model commands. Names must be unique.",
|
||||||
"presetDisplayName": "Display name",
|
|
||||||
"presetDisplayNameHelp": "Shown in the interface. The command name stays /model {{name}}.",
|
|
||||||
"presetNamePlaceholder": "Fast writing",
|
"presetNamePlaceholder": "Fast writing",
|
||||||
"advancedOptions": "Advanced options",
|
"advancedOptions": "Advanced options",
|
||||||
"advancedSummary": "Context {{context}} · Max {{max}} tokens",
|
"advancedSummary": "Context {{context}} · Max {{max}} tokens",
|
||||||
|
|||||||
@@ -419,9 +419,7 @@
|
|||||||
"presets": "Preajustes de modelo",
|
"presets": "Preajustes de modelo",
|
||||||
"editPreset": "Editar preajuste",
|
"editPreset": "Editar preajuste",
|
||||||
"presetName": "Nombre del preajuste",
|
"presetName": "Nombre del preajuste",
|
||||||
"presetNameHelp": "Un nombre corto para usar en los ajustes de modelos.",
|
"presetNameHelp": "Se usa en la interfaz y en /model. Los nombres deben ser únicos.",
|
||||||
"presetDisplayName": "Nombre para mostrar",
|
|
||||||
"presetDisplayNameHelp": "Se muestra en la interfaz. El nombre del comando sigue siendo /model {{name}}.",
|
|
||||||
"presetNamePlaceholder": "Escritura rápida",
|
"presetNamePlaceholder": "Escritura rápida",
|
||||||
"advancedOptions": "Opciones avanzadas",
|
"advancedOptions": "Opciones avanzadas",
|
||||||
"advancedSummary": "Contexto {{context}} · Máx. {{max}} tokens",
|
"advancedSummary": "Contexto {{context}} · Máx. {{max}} tokens",
|
||||||
|
|||||||
@@ -419,9 +419,7 @@
|
|||||||
"presets": "Préréglages de modèle",
|
"presets": "Préréglages de modèle",
|
||||||
"editPreset": "Modifier le préréglage",
|
"editPreset": "Modifier le préréglage",
|
||||||
"presetName": "Nom du préréglage",
|
"presetName": "Nom du préréglage",
|
||||||
"presetNameHelp": "Un nom court utilisé dans les paramètres des modèles.",
|
"presetNameHelp": "Utilisé dans l’interface et avec /model. Les noms doivent être uniques.",
|
||||||
"presetDisplayName": "Nom d’affichage",
|
|
||||||
"presetDisplayNameHelp": "Affiché dans l’interface. Le nom de commande reste /model {{name}}.",
|
|
||||||
"presetNamePlaceholder": "Rédaction rapide",
|
"presetNamePlaceholder": "Rédaction rapide",
|
||||||
"advancedOptions": "Options avancées",
|
"advancedOptions": "Options avancées",
|
||||||
"advancedSummary": "Contexte {{context}} · Max. {{max}} tokens",
|
"advancedSummary": "Contexte {{context}} · Max. {{max}} tokens",
|
||||||
|
|||||||
@@ -419,9 +419,7 @@
|
|||||||
"presets": "Prasetel model",
|
"presets": "Prasetel model",
|
||||||
"editPreset": "Ubah prasetel",
|
"editPreset": "Ubah prasetel",
|
||||||
"presetName": "Nama prasetel",
|
"presetName": "Nama prasetel",
|
||||||
"presetNameHelp": "Nama singkat yang digunakan di pengaturan model.",
|
"presetNameHelp": "Digunakan di antarmuka dan perintah /model. Nama harus unik.",
|
||||||
"presetDisplayName": "Nama tampilan",
|
|
||||||
"presetDisplayNameHelp": "Ditampilkan di antarmuka. Nama perintah tetap /model {{name}}.",
|
|
||||||
"presetNamePlaceholder": "Menulis cepat",
|
"presetNamePlaceholder": "Menulis cepat",
|
||||||
"advancedOptions": "Opsi lanjutan",
|
"advancedOptions": "Opsi lanjutan",
|
||||||
"advancedSummary": "Konteks {{context}} · Maks. {{max}} token",
|
"advancedSummary": "Konteks {{context}} · Maks. {{max}} token",
|
||||||
|
|||||||
@@ -419,9 +419,7 @@
|
|||||||
"presets": "モデルプリセット",
|
"presets": "モデルプリセット",
|
||||||
"editPreset": "プリセットを編集",
|
"editPreset": "プリセットを編集",
|
||||||
"presetName": "プリセット名",
|
"presetName": "プリセット名",
|
||||||
"presetNameHelp": "モデル設定で使用する短い名前です。",
|
"presetNameHelp": "画面と /model コマンドで使用します。名前は一意である必要があります。",
|
||||||
"presetDisplayName": "表示名",
|
|
||||||
"presetDisplayNameHelp": "画面に表示される名前です。コマンド名は /model {{name}} のままです。",
|
|
||||||
"presetNamePlaceholder": "高速執筆",
|
"presetNamePlaceholder": "高速執筆",
|
||||||
"advancedOptions": "詳細オプション",
|
"advancedOptions": "詳細オプション",
|
||||||
"advancedSummary": "コンテキスト {{context}} · 最大 {{max}} トークン",
|
"advancedSummary": "コンテキスト {{context}} · 最大 {{max}} トークン",
|
||||||
|
|||||||
@@ -419,9 +419,7 @@
|
|||||||
"presets": "모델 프리셋",
|
"presets": "모델 프리셋",
|
||||||
"editPreset": "프리셋 편집",
|
"editPreset": "프리셋 편집",
|
||||||
"presetName": "프리셋 이름",
|
"presetName": "프리셋 이름",
|
||||||
"presetNameHelp": "모델 설정에서 사용할 짧은 이름입니다.",
|
"presetNameHelp": "화면과 /model 명령에서 사용됩니다. 이름은 고유해야 합니다.",
|
||||||
"presetDisplayName": "표시 이름",
|
|
||||||
"presetDisplayNameHelp": "화면에 표시되는 이름입니다. 명령 이름은 /model {{name}} 그대로 유지됩니다.",
|
|
||||||
"presetNamePlaceholder": "빠른 작성",
|
"presetNamePlaceholder": "빠른 작성",
|
||||||
"advancedOptions": "고급 옵션",
|
"advancedOptions": "고급 옵션",
|
||||||
"advancedSummary": "컨텍스트 {{context}} · 최대 {{max}} 토큰",
|
"advancedSummary": "컨텍스트 {{context}} · 최대 {{max}} 토큰",
|
||||||
|
|||||||
@@ -143,9 +143,7 @@
|
|||||||
"presets": "Predefinições de modelo",
|
"presets": "Predefinições de modelo",
|
||||||
"editPreset": "Editar predefinição",
|
"editPreset": "Editar predefinição",
|
||||||
"presetName": "Nome da predefinição",
|
"presetName": "Nome da predefinição",
|
||||||
"presetNameHelp": "Um nome curto usado nas configurações de modelo.",
|
"presetNameHelp": "Usado na interface e nos comandos /model. Os nomes devem ser únicos.",
|
||||||
"presetDisplayName": "Nome de exibição",
|
|
||||||
"presetDisplayNameHelp": "Exibido na interface. O nome do comando continua sendo /model {{name}}.",
|
|
||||||
"presetNamePlaceholder": "Escrita rápida",
|
"presetNamePlaceholder": "Escrita rápida",
|
||||||
"advancedOptions": "Opções avançadas",
|
"advancedOptions": "Opções avançadas",
|
||||||
"advancedSummary": "Contexto {{context}} · Máx. {{max}} tokens",
|
"advancedSummary": "Contexto {{context}} · Máx. {{max}} tokens",
|
||||||
|
|||||||
@@ -419,9 +419,7 @@
|
|||||||
"presets": "Cấu hình mô hình đặt trước",
|
"presets": "Cấu hình mô hình đặt trước",
|
||||||
"editPreset": "Sửa cấu hình đặt trước",
|
"editPreset": "Sửa cấu hình đặt trước",
|
||||||
"presetName": "Tên cấu hình đặt trước",
|
"presetName": "Tên cấu hình đặt trước",
|
||||||
"presetNameHelp": "Tên ngắn dùng trong phần thiết lập mô hình.",
|
"presetNameHelp": "Dùng trong giao diện và lệnh /model. Tên phải là duy nhất.",
|
||||||
"presetDisplayName": "Tên hiển thị",
|
|
||||||
"presetDisplayNameHelp": "Tên được hiển thị trong giao diện. Tên lệnh vẫn là /model {{name}}.",
|
|
||||||
"presetNamePlaceholder": "Viết nhanh",
|
"presetNamePlaceholder": "Viết nhanh",
|
||||||
"advancedOptions": "Tùy chọn nâng cao",
|
"advancedOptions": "Tùy chọn nâng cao",
|
||||||
"advancedSummary": "Ngữ cảnh {{context}} · Tối đa {{max}} token",
|
"advancedSummary": "Ngữ cảnh {{context}} · Tối đa {{max}} token",
|
||||||
|
|||||||
@@ -143,9 +143,7 @@
|
|||||||
"presets": "模型预设",
|
"presets": "模型预设",
|
||||||
"editPreset": "编辑预设",
|
"editPreset": "编辑预设",
|
||||||
"presetName": "预设名称",
|
"presetName": "预设名称",
|
||||||
"presetNameHelp": "在模型设置中使用的简短名称。",
|
"presetNameHelp": "用于界面和 /model 命令;名称不能重复。",
|
||||||
"presetDisplayName": "显示名称",
|
|
||||||
"presetDisplayNameHelp": "显示在界面中;命令名称仍为 /model {{name}}。",
|
|
||||||
"presetNamePlaceholder": "快速写作",
|
"presetNamePlaceholder": "快速写作",
|
||||||
"advancedOptions": "高级选项",
|
"advancedOptions": "高级选项",
|
||||||
"advancedSummary": "上下文 {{context}} · 最大输出 {{max}} tokens",
|
"advancedSummary": "上下文 {{context}} · 最大输出 {{max}} tokens",
|
||||||
|
|||||||
@@ -419,9 +419,7 @@
|
|||||||
"presets": "模型預設",
|
"presets": "模型預設",
|
||||||
"editPreset": "編輯預設",
|
"editPreset": "編輯預設",
|
||||||
"presetName": "預設名稱",
|
"presetName": "預設名稱",
|
||||||
"presetNameHelp": "模型設定中使用的簡短名稱。",
|
"presetNameHelp": "用於介面和 /model 命令;名稱不得重複。",
|
||||||
"presetDisplayName": "顯示名稱",
|
|
||||||
"presetDisplayNameHelp": "顯示在介面中;命令名稱仍為 /model {{name}}。",
|
|
||||||
"presetNamePlaceholder": "快速寫作",
|
"presetNamePlaceholder": "快速寫作",
|
||||||
"advancedOptions": "進階選項",
|
"advancedOptions": "進階選項",
|
||||||
"advancedSummary": "上下文 {{context}} · 最大輸出 {{max}} tokens",
|
"advancedSummary": "上下文 {{context}} · 最大輸出 {{max}} tokens",
|
||||||
|
|||||||
@@ -910,8 +910,7 @@ export async function createModelConfiguration(
|
|||||||
transport,
|
transport,
|
||||||
"settings.model_configuration.create",
|
"settings.model_configuration.create",
|
||||||
{
|
{
|
||||||
...(configuration.name !== undefined ? { name: configuration.name } : {}),
|
name: configuration.name,
|
||||||
label: configuration.label,
|
|
||||||
provider: configuration.provider,
|
provider: configuration.provider,
|
||||||
model: configuration.model,
|
model: configuration.model,
|
||||||
...modelGenerationSettingsPayload(configuration),
|
...modelGenerationSettingsPayload(configuration),
|
||||||
@@ -928,7 +927,6 @@ export async function updateModelConfiguration(
|
|||||||
"settings.model_configuration.update",
|
"settings.model_configuration.update",
|
||||||
{
|
{
|
||||||
name: configuration.name,
|
name: configuration.name,
|
||||||
...(configuration.label !== undefined ? { label: configuration.label } : {}),
|
|
||||||
...(configuration.provider !== undefined ? { provider: configuration.provider } : {}),
|
...(configuration.provider !== undefined ? { provider: configuration.provider } : {}),
|
||||||
...(configuration.model !== undefined ? { model: configuration.model } : {}),
|
...(configuration.model !== undefined ? { model: configuration.model } : {}),
|
||||||
...modelGenerationSettingsPayload(configuration),
|
...modelGenerationSettingsPayload(configuration),
|
||||||
|
|||||||
@@ -522,7 +522,8 @@ export interface SettingsPayload {
|
|||||||
};
|
};
|
||||||
model_presets: Array<{
|
model_presets: Array<{
|
||||||
name: string;
|
name: string;
|
||||||
label: string;
|
/** @deprecated Compatibility alias. New clients must use `name`. */
|
||||||
|
label?: string;
|
||||||
active: boolean;
|
active: boolean;
|
||||||
is_default: boolean;
|
is_default: boolean;
|
||||||
model: string;
|
model: string;
|
||||||
@@ -1085,8 +1086,7 @@ export interface SettingsUpdate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ModelConfigurationCreate {
|
export interface ModelConfigurationCreate {
|
||||||
name?: string;
|
name: string;
|
||||||
label: string;
|
|
||||||
provider: string;
|
provider: string;
|
||||||
model: string;
|
model: string;
|
||||||
maxTokens?: number;
|
maxTokens?: number;
|
||||||
@@ -1097,7 +1097,6 @@ export interface ModelConfigurationCreate {
|
|||||||
|
|
||||||
export interface ModelConfigurationUpdate {
|
export interface ModelConfigurationUpdate {
|
||||||
name: string;
|
name: string;
|
||||||
label?: string;
|
|
||||||
provider?: string;
|
provider?: string;
|
||||||
model?: string;
|
model?: string;
|
||||||
maxTokens?: number;
|
maxTokens?: number;
|
||||||
|
|||||||
@@ -439,7 +439,7 @@ describe("webui API helpers", () => {
|
|||||||
|
|
||||||
it("serializes model configuration creation", async () => {
|
it("serializes model configuration creation", async () => {
|
||||||
await createModelConfiguration(mutationTransport, {
|
await createModelConfiguration(mutationTransport, {
|
||||||
label: "Fast writing",
|
name: "Fast writing",
|
||||||
provider: "openai",
|
provider: "openai",
|
||||||
model: "openai/gpt-4.1-mini",
|
model: "openai/gpt-4.1-mini",
|
||||||
maxTokens: 4096,
|
maxTokens: 4096,
|
||||||
@@ -451,7 +451,7 @@ describe("webui API helpers", () => {
|
|||||||
expect(requestMutation).toHaveBeenCalledWith(
|
expect(requestMutation).toHaveBeenCalledWith(
|
||||||
"settings.model_configuration.create",
|
"settings.model_configuration.create",
|
||||||
{
|
{
|
||||||
label: "Fast writing",
|
name: "Fast writing",
|
||||||
provider: "openai",
|
provider: "openai",
|
||||||
model: "openai/gpt-4.1-mini",
|
model: "openai/gpt-4.1-mini",
|
||||||
max_tokens: 4096,
|
max_tokens: 4096,
|
||||||
@@ -466,7 +466,6 @@ describe("webui API helpers", () => {
|
|||||||
it("serializes model configuration updates", async () => {
|
it("serializes model configuration updates", async () => {
|
||||||
await updateModelConfiguration(mutationTransport, {
|
await updateModelConfiguration(mutationTransport, {
|
||||||
name: "codex",
|
name: "codex",
|
||||||
label: "Codex",
|
|
||||||
provider: "openai_codex",
|
provider: "openai_codex",
|
||||||
model: "openai-codex/gpt-5.5",
|
model: "openai-codex/gpt-5.5",
|
||||||
maxTokens: 8192,
|
maxTokens: 8192,
|
||||||
@@ -479,7 +478,6 @@ describe("webui API helpers", () => {
|
|||||||
"settings.model_configuration.update",
|
"settings.model_configuration.update",
|
||||||
{
|
{
|
||||||
name: "codex",
|
name: "codex",
|
||||||
label: "Codex",
|
|
||||||
provider: "openai_codex",
|
provider: "openai_codex",
|
||||||
model: "openai-codex/gpt-5.5",
|
model: "openai-codex/gpt-5.5",
|
||||||
max_tokens: 8192,
|
max_tokens: 8192,
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ async function togglePresetEditor(name = "primary") {
|
|||||||
describe("Settings models", () => {
|
describe("Settings models", () => {
|
||||||
installSettingsViewTestHooks();
|
installSettingsViewTestHooks();
|
||||||
|
|
||||||
it("distinguishes the editable display name from the stable command name", async () => {
|
it("uses the preset name as the canonical identity", async () => {
|
||||||
const payload = settingsPayload();
|
const payload = settingsPayload();
|
||||||
payload.model_presets[0] = {
|
payload.model_presets[0] = {
|
||||||
...payload.model_presets[0],
|
...payload.model_presets[0],
|
||||||
@@ -104,11 +104,10 @@ describe("Settings models", () => {
|
|||||||
|
|
||||||
await togglePresetEditor("openai");
|
await togglePresetEditor("openai");
|
||||||
|
|
||||||
expect(screen.getByText("Display name")).toBeInTheDocument();
|
const editor = screen.getByTestId("model-preset-editor");
|
||||||
expect(
|
expect(within(editor).getByText("Preset name")).toBeInTheDocument();
|
||||||
screen.getByText("Shown in the interface. The command name stays /model openai."),
|
expect(within(editor).getByText("openai")).toBeInTheDocument();
|
||||||
).toBeInTheDocument();
|
expect(within(editor).queryByText("minimax")).not.toBeInTheDocument();
|
||||||
expect(screen.getByDisplayValue("minimax")).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps generation parameters collapsed until advanced options are opened", async () => {
|
it("keeps generation parameters collapsed until advanced options are opened", async () => {
|
||||||
@@ -188,7 +187,7 @@ describe("Settings models", () => {
|
|||||||
"lg:max-w-6xl",
|
"lg:max-w-6xl",
|
||||||
"rounded-floating",
|
"rounded-floating",
|
||||||
);
|
);
|
||||||
expect(within(editor).getByDisplayValue("Primary")).toBeInTheDocument();
|
expect(within(editor).getByText("primary")).toBeInTheDocument();
|
||||||
const deleteButton = within(editor).getByRole("button", { name: "Delete" });
|
const deleteButton = within(editor).getByRole("button", { name: "Delete" });
|
||||||
expect(deleteButton).toBeDisabled();
|
expect(deleteButton).toBeDisabled();
|
||||||
expect(deleteButton).toHaveAttribute("aria-describedby", "model-preset-delete-hint");
|
expect(deleteButton).toHaveAttribute("aria-describedby", "model-preset-delete-hint");
|
||||||
@@ -244,8 +243,9 @@ describe("Settings models", () => {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
await togglePresetEditor();
|
await togglePresetEditor();
|
||||||
fireEvent.change(screen.getByDisplayValue("Primary"), {
|
fireEvent.click(screen.getByRole("button", { name: /Advanced options/ }));
|
||||||
target: { value: "Primary draft" },
|
fireEvent.change(screen.getByLabelText("Temperature"), {
|
||||||
|
target: { value: "0.4" },
|
||||||
});
|
});
|
||||||
const primaryRow = screen.getByTestId("model-call-order-row-primary");
|
const primaryRow = screen.getByTestId("model-call-order-row-primary");
|
||||||
const backupRow = screen.getByTestId("model-call-order-row-backup");
|
const backupRow = screen.getByTestId("model-call-order-row-backup");
|
||||||
@@ -270,7 +270,7 @@ describe("Settings models", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(screen.queryByRole("button", { name: "Save order" })).not.toBeInTheDocument();
|
expect(screen.queryByRole("button", { name: "Save order" })).not.toBeInTheDocument();
|
||||||
expect(screen.getByDisplayValue("Primary draft")).toBeInTheDocument();
|
expect(screen.getByLabelText("Temperature")).toHaveValue(0.4);
|
||||||
expect(screen.getByRole("button", { name: "Save preset" })).toBeEnabled();
|
expect(screen.getByRole("button", { name: "Save preset" })).toBeEnabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -430,7 +430,8 @@ describe("Settings models", () => {
|
|||||||
renderSettingsView({ initialSection: "models", initialSettings: payloadWithCodex });
|
renderSettingsView({ initialSection: "models", initialSettings: payloadWithCodex });
|
||||||
|
|
||||||
const codexRow = await screen.findByTestId("model-call-order-row-codex");
|
const codexRow = await screen.findByTestId("model-call-order-row-codex");
|
||||||
expect(codexRow).toHaveTextContent("Codex");
|
expect(codexRow).toHaveTextContent("codex");
|
||||||
|
expect(codexRow).not.toHaveTextContent("Codex");
|
||||||
expect(codexRow).toHaveTextContent("openai-codex/gpt-5.5");
|
expect(codexRow).toHaveTextContent("openai-codex/gpt-5.5");
|
||||||
expect(codexRow).toHaveTextContent("Disabled");
|
expect(codexRow).toHaveTextContent("Disabled");
|
||||||
expect(codexRow).toHaveAttribute("draggable", "false");
|
expect(codexRow).toHaveAttribute("draggable", "false");
|
||||||
@@ -461,7 +462,7 @@ describe("Settings models", () => {
|
|||||||
const { payload } = settingsPayloadWithBackup();
|
const { payload } = settingsPayloadWithBackup();
|
||||||
const writerPreset = {
|
const writerPreset = {
|
||||||
...payload.model_presets[0],
|
...payload.model_presets[0],
|
||||||
name: "writer",
|
name: "Writer",
|
||||||
label: "Writer",
|
label: "Writer",
|
||||||
active: false,
|
active: false,
|
||||||
model: "openai/gpt-4o-mini",
|
model: "openai/gpt-4o-mini",
|
||||||
@@ -475,7 +476,7 @@ describe("Settings models", () => {
|
|||||||
};
|
};
|
||||||
const orderedPayload: SettingsPayload = {
|
const orderedPayload: SettingsPayload = {
|
||||||
...createdPayload,
|
...createdPayload,
|
||||||
model_call_order: ["primary", "backup", "writer"],
|
model_call_order: ["primary", "backup", "Writer"],
|
||||||
created_model_preset: undefined,
|
created_model_preset: undefined,
|
||||||
};
|
};
|
||||||
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
|
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
|
||||||
@@ -528,15 +529,15 @@ describe("Settings models", () => {
|
|||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(requestMutationMock).toHaveBeenLastCalledWith(
|
expect(requestMutationMock).toHaveBeenLastCalledWith(
|
||||||
"settings.model_call_order.update",
|
"settings.model_call_order.update",
|
||||||
{ order: ["primary", "backup", "writer"] },
|
{ order: ["primary", "backup", "Writer"] },
|
||||||
20_000,
|
20_000,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
const writerRow = await screen.findByTestId("model-call-order-row-writer");
|
const writerRow = await screen.findByTestId("model-call-order-row-Writer");
|
||||||
expect(writerRow).not.toHaveTextContent("Disabled");
|
expect(writerRow).not.toHaveTextContent("Disabled");
|
||||||
expect(writerRow).not.toHaveTextContent(/Fallback/);
|
expect(writerRow).not.toHaveTextContent(/Fallback/);
|
||||||
expect(within(writerRow).getByRole("switch", { name: "Disable preset" })).toBeChecked();
|
expect(within(writerRow).getByRole("switch", { name: "Disable preset" })).toBeChecked();
|
||||||
expect(screen.getByDisplayValue("Writer")).toBeInTheDocument();
|
expect(screen.getAllByText("Writer").length).toBeGreaterThan(0);
|
||||||
expect(screen.queryByRole("button", { name: "Save order" })).not.toBeInTheDocument();
|
expect(screen.queryByRole("button", { name: "Save order" })).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -307,9 +307,9 @@ function ascii(bytes: Uint8Array, offset: number, length: number): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const MODEL_PRESETS = [
|
const MODEL_PRESETS = [
|
||||||
{ name: "kimi", label: "Kimi", provider: "moonshot" },
|
{ name: "kimi", provider: "moonshot" },
|
||||||
{ name: "dflash", label: "DFlash", provider: "deepseek" },
|
{ name: "dflash", provider: "deepseek" },
|
||||||
{ name: "dspro", label: "DS Pro", provider: "deepseek" },
|
{ name: "dspro", provider: "deepseek" },
|
||||||
];
|
];
|
||||||
|
|
||||||
function renderPresetComposer(variant: "thread" | "hero" = "thread") {
|
function renderPresetComposer(variant: "thread" | "hero" = "thread") {
|
||||||
@@ -317,7 +317,7 @@ function renderPresetComposer(variant: "thread" | "hero" = "thread") {
|
|||||||
render(
|
render(
|
||||||
<ThreadComposer
|
<ThreadComposer
|
||||||
onSend={vi.fn()}
|
onSend={vi.fn()}
|
||||||
modelLabel="Kimi"
|
modelLabel="kimi"
|
||||||
modelPreset="kimi"
|
modelPreset="kimi"
|
||||||
modelProvider="moonshot"
|
modelProvider="moonshot"
|
||||||
modelPresets={MODEL_PRESETS}
|
modelPresets={MODEL_PRESETS}
|
||||||
@@ -327,7 +327,7 @@ function renderPresetComposer(variant: "thread" | "hero" = "thread") {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
badge: screen.getByRole("spinbutton", { name: "Kimi" }),
|
badge: screen.getByRole("spinbutton", { name: "kimi" }),
|
||||||
onPresetChange,
|
onPresetChange,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -604,7 +604,7 @@ describe("ThreadComposer", () => {
|
|||||||
expect(Array.from(pills).every((pill) => pill.querySelector("img"))).toBe(true);
|
expect(Array.from(pills).every((pill) => pill.querySelector("img"))).toBe(true);
|
||||||
expect(Array.from(badge.querySelectorAll("img")).every((image) => !image.draggable)).toBe(true);
|
expect(Array.from(badge.querySelectorAll("img")).every((image) => !image.draggable)).toBe(true);
|
||||||
const centeredPill = track.querySelector<HTMLElement>("[data-preset-offset='0']");
|
const centeredPill = track.querySelector<HTMLElement>("[data-preset-offset='0']");
|
||||||
expect(centeredPill).toHaveTextContent("Kimi");
|
expect(centeredPill).toHaveTextContent("kimi");
|
||||||
expect(centeredPill).toHaveStyle({ transform: "scale(1.0800)" });
|
expect(centeredPill).toHaveStyle({ transform: "scale(1.0800)" });
|
||||||
expect(
|
expect(
|
||||||
track.querySelector<HTMLElement>("[data-preset-offset='1']"),
|
track.querySelector<HTMLElement>("[data-preset-offset='1']"),
|
||||||
@@ -615,13 +615,13 @@ describe("ThreadComposer", () => {
|
|||||||
pointerId: 7,
|
pointerId: 7,
|
||||||
pointerType: "mouse",
|
pointerType: "mouse",
|
||||||
});
|
});
|
||||||
expect(track.querySelector("[data-preset-offset='0']")).toHaveTextContent("Kimi");
|
expect(track.querySelector("[data-preset-offset='0']")).toHaveTextContent("kimi");
|
||||||
fireEvent.pointerMove(badge, {
|
fireEvent.pointerMove(badge, {
|
||||||
clientY: 123,
|
clientY: 123,
|
||||||
pointerId: 7,
|
pointerId: 7,
|
||||||
pointerType: "mouse",
|
pointerType: "mouse",
|
||||||
});
|
});
|
||||||
expect(track.querySelector("[data-preset-offset='0']")).toHaveTextContent("DS Pro");
|
expect(track.querySelector("[data-preset-offset='0']")).toHaveTextContent("dspro");
|
||||||
fireEvent.pointerUp(badge, {
|
fireEvent.pointerUp(badge, {
|
||||||
clientY: 123,
|
clientY: 123,
|
||||||
pointerId: 7,
|
pointerId: 7,
|
||||||
|
|||||||
@@ -609,7 +609,7 @@ describe("ThreadShell", () => {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(await screen.findByTitle("Fast · gpt-5.5 · OpenAI Codex")).toBeInTheDocument();
|
expect(await screen.findByTitle("fast · gpt-5.5 · OpenAI Codex")).toBeInTheDocument();
|
||||||
expect(screen.queryByTitle("Default · deepseek-v4-pro · DeepSeek")).not.toBeInTheDocument();
|
expect(screen.queryByTitle("Default · deepseek-v4-pro · DeepSeek")).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -645,19 +645,19 @@ describe("ThreadShell", () => {
|
|||||||
"preset-order",
|
"preset-order",
|
||||||
"/model fast",
|
"/model fast",
|
||||||
);
|
);
|
||||||
expect(await screen.findByText("Fast")).toBeInTheDocument();
|
expect(await screen.findByText("fast")).toBeInTheDocument();
|
||||||
fireEvent.keyDown(
|
fireEvent.keyDown(
|
||||||
screen.getByRole("spinbutton", { name: "Fast" }),
|
screen.getByRole("spinbutton", { name: "fast" }),
|
||||||
{ key: "End" },
|
{ key: "End" },
|
||||||
);
|
);
|
||||||
expect(client.sendSystemCommand).toHaveBeenLastCalledWith(
|
expect(client.sendSystemCommand).toHaveBeenLastCalledWith(
|
||||||
"preset-order",
|
"preset-order",
|
||||||
"/model extra",
|
"/model extra",
|
||||||
);
|
);
|
||||||
expect(await screen.findByText("Extra")).toBeInTheDocument();
|
expect(await screen.findByText("extra")).toBeInTheDocument();
|
||||||
|
|
||||||
rerender(view("fast"));
|
rerender(view("fast"));
|
||||||
expect(await screen.findByText("Fast")).toBeInTheDocument();
|
expect(await screen.findByText("fast")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses the backend-resolved provider for an auto session preset", async () => {
|
it("uses the backend-resolved provider for an auto session preset", async () => {
|
||||||
@@ -692,7 +692,7 @@ describe("ThreadShell", () => {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(await screen.findByTitle("Fast · gpt-4 · Company Proxy")).toBeInTheDocument();
|
expect(await screen.findByTitle("fast · gpt-4 · Company Proxy")).toBeInTheDocument();
|
||||||
expect(screen.queryByRole("button", { name: "Model not configured" })).not.toBeInTheDocument();
|
expect(screen.queryByRole("button", { name: "Model not configured" })).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1070,7 +1070,7 @@ describe("ThreadShell", () => {
|
|||||||
await screen.findByRole("spinbutton", { name: "Default" }),
|
await screen.findByRole("spinbutton", { name: "Default" }),
|
||||||
{ key: "ArrowDown" },
|
{ key: "ArrowDown" },
|
||||||
);
|
);
|
||||||
expect(await screen.findByText("Fast")).toBeInTheDocument();
|
expect(await screen.findByText("fast")).toBeInTheDocument();
|
||||||
expect(client.sendSystemCommand).not.toHaveBeenCalled();
|
expect(client.sendSystemCommand).not.toHaveBeenCalled();
|
||||||
|
|
||||||
fireEvent.change(screen.getByLabelText("Message input"), {
|
fireEvent.change(screen.getByLabelText("Message input"), {
|
||||||
|
|||||||
Reference in New Issue
Block a user