feat(code): show model name instead of spec in switcher (#4460)

The `/model` switcher now shows just the model name under each provider
header instead of the full `provider:model` string.

---

In the `/model` selector (`libs/code`), provider-grouped rows rendered
the full `provider:model` spec even though each row sits under a header
that already names the provider. This shows just the model name for
those rows, reducing redundancy. A `show_provider_prefix` flag is
threaded through the label helpers and persisted on each `ModelOption`
so incremental relabels in `_move_selection` stay consistent with the
full rebuild. The pinned "Recent" section keeps the full spec since it
has no provider header, and all selection/matching logic still keys off
the full spec.

Made by [Open
SWE](https://openswe.vercel.app/agents/9a970f12-372e-9bdb-1fd6-57126025f5c2)

---------

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
This commit is contained in:
Mason Daugherty
2026-07-03 02:13:02 -04:00
committed by GitHub
parent ccd9d216e7
commit 005962088a
7 changed files with 679 additions and 90 deletions
+29
View File
@@ -438,6 +438,13 @@ class ProviderConfig(TypedDict, total=False):
capitalization.
"""
short_name: str
"""Compact brand label for space-constrained UI (e.g. the `/model` Recent
tag), where the full `display_name` — which may carry a parenthetical
qualifier like `"OpenAI Codex (ChatGPT login)"` — is too long. Optional;
when unset, callers fall back to `display_name`.
"""
api_key_url: str
"""Provider page where users can create or manage API keys.
@@ -2430,6 +2437,15 @@ class ModelConfig:
display_name,
)
short_name = cast("object", provider.get("short_name"))
if short_name is not None and not isinstance(short_name, str):
logger.warning(
"Provider '%s' has non-string 'short_name' value %r "
"(expected a string). Falling back to the display name.",
name,
short_name,
)
api_key_url = cast("object", provider.get("api_key_url"))
if api_key_url is not None and not isinstance(api_key_url, str):
logger.warning(
@@ -2618,6 +2634,19 @@ class ModelConfig:
name = provider.get("display_name") if provider else None
return name if isinstance(name, str) else None
def get_provider_short_name(self, provider_name: str) -> str | None:
"""Get the configured compact brand name for a provider.
Args:
provider_name: The provider to look up.
Returns:
Compact brand name if configured, None otherwise.
"""
provider = self.providers.get(provider_name)
name = provider.get("short_name") if provider else None
return name if isinstance(name, str) else None
def get_provider_api_key_url(self, provider_name: str) -> str | None:
"""Get the configured API-key management URL for a provider.
+48 -5
View File
@@ -102,6 +102,20 @@ PROVIDER_DISPLAY_NAMES: dict[str, str] = {
}
PROVIDER_SHORT_NAMES: dict[str, str] = {
# Only providers whose `PROVIDER_DISPLAY_NAMES` label is too verbose for a
# compact tag need an entry here; everything else falls back to the display
# name, which is already short.
"openai_codex": "OpenAI Codex",
}
"""Compact brand labels for space-constrained UI (e.g. the `/model` Recent tag).
Sparse companion to `PROVIDER_DISPLAY_NAMES`: an entry exists only when the full
display name carries a parenthetical qualifier that reads badly inside a tag
(e.g. `"OpenAI Codex (ChatGPT login)"`). Resolved via `provider_short_name`.
"""
PROVIDER_API_KEY_URLS: dict[str, str] = {
"anthropic": "https://platform.claude.com/login?returnTo=%2Fsettings%2Fkeys",
"baseten": "https://docs.baseten.co/organization/api-keys",
@@ -141,8 +155,14 @@ def _is_safe_acquisition_url(url: str) -> bool:
return urlsplit(url).scheme in {"http", "https"}
def _provider_display_name(provider: str, config: ModelConfig | None = None) -> str:
"""Return a human-readable provider label for auth UI.
def provider_display_name(provider: str, config: ModelConfig | None = None) -> str:
"""Return a human-readable provider label.
Shared by the auth UI and the model selector so a provider is labeled
identically in both. (The install prompt reuses the underlying
`PROVIDER_DISPLAY_NAMES` map directly rather than this function, to avoid an
event-loop config read, so a user-configured `display_name` won't surface
there.)
Resolution order: a configured `display_name`, then the built-in
`PROVIDER_DISPLAY_NAMES` map, then a title-cased form of the provider key.
@@ -160,6 +180,29 @@ def _provider_display_name(provider: str, config: ModelConfig | None = None) ->
) or PROVIDER_DISPLAY_NAMES.get(provider, provider.replace("_", " ").title())
def provider_short_name(provider: str, config: ModelConfig | None = None) -> str:
"""Return a compact brand label for a provider.
For space-constrained UI (e.g. the `/model` Recent tag). Resolution order:
a configured `short_name`, then the built-in `PROVIDER_SHORT_NAMES` map,
then the full `provider_display_name` (which is already short for providers
without a parenthetical qualifier).
Args:
provider: Provider config key.
config: Parsed model config, if already loaded by the caller.
Returns:
Compact brand label, falling back to the display name when none is set.
"""
model_config = config or ModelConfig.load()
return (
model_config.get_provider_short_name(provider)
or PROVIDER_SHORT_NAMES.get(provider)
or provider_display_name(provider, model_config)
)
def _auth_status_for(provider: str) -> ProviderAuthStatus:
"""Resolve the credential readiness of a provider or non-model service.
@@ -558,7 +601,7 @@ class AuthPromptScreen(ModalScreen[AuthResult]):
Widgets that make up the auth prompt modal.
"""
glyphs = get_glyphs()
provider_label = _provider_display_name(self._provider, self._config)
provider_label = provider_display_name(self._provider, self._config)
with Vertical():
# Tag the title with `(stored)` so the user knows a replacement
# (or the `Ctrl+D delete` affordance shown in the help line) is
@@ -801,7 +844,7 @@ class AuthPromptScreen(ModalScreen[AuthResult]):
url = configured_url or PROVIDER_API_KEY_URLS.get(
self._provider, _PROVIDERS_DOCS_URL
)
provider = _provider_display_name(self._provider, config)
provider = provider_display_name(self._provider, config)
label = (
f"{provider} key page"
if configured_url or self._provider in PROVIDER_API_KEY_URLS
@@ -1532,7 +1575,7 @@ class AuthManagerScreen(ModalScreen[None]):
Returns:
A composed `Content` with the provider label and a status badge.
"""
name = _provider_display_name(provider)
name = provider_display_name(provider)
if not installed:
return Content.assemble(
Content.styled(name, "dim"),
@@ -195,7 +195,7 @@ class InstallProviderConfirmScreen(ModalScreen[bool]):
# Reuse the auth UI's curated labels (e.g. `google_genai` -> "Google
# Gemini") so the title reads naturally, falling back to a title-cased
# provider key. Avoids the event-loop config read in
# `_provider_display_name`, which is overkill for a static title.
# `provider_display_name`, which is overkill for a static title.
from deepagents_code.widgets.auth import PROVIDER_DISPLAY_NAMES
provider = PROVIDER_DISPLAY_NAMES.get(
@@ -30,6 +30,7 @@ from deepagents_code.model_config import (
CODEX_PROVIDER,
ModelConfig,
ModelProfileEntry,
ModelSpec,
ProviderAuthState,
ProviderAuthStatus,
clear_default_model,
@@ -63,66 +64,73 @@ from the per-provider sections below.
"""
_RECOMMENDED_MODELS: frozenset[str] = frozenset(
{
"anthropic:claude-opus-4-7",
"anthropic:claude-opus-4-8",
"anthropic:claude-sonnet-5",
"baseten:deepseek-ai/DeepSeek-V4-Pro",
"baseten:moonshotai/Kimi-K2.7-Code",
"baseten:nvidia/NVIDIA-Nemotron-3-Ultra-550B-A55B",
"baseten:zai-org/GLM-5.2",
"fireworks:accounts/fireworks/models/deepseek-v4-pro",
"fireworks:accounts/fireworks/models/glm-5p2",
"fireworks:accounts/fireworks/models/kimi-k2p7-code",
"fireworks:accounts/fireworks/models/minimax-m3",
"fireworks:accounts/fireworks/models/qwen3p7-plus",
"google_genai:gemini-3.5-flash",
"google_genai:gemini-3.1-pro-preview",
"ollama:deepseek-v4-flash:cloud",
"ollama:deepseek-v4-pro:cloud",
"ollama:glm-5.2:cloud",
"ollama:kimi-k2.7-code:cloud",
"ollama:minimax-m3:cloud",
"openai:gpt-5.4",
"openai:gpt-5.4-mini",
"openai:gpt-5.4-pro",
"openai:gpt-5.5",
"openai:gpt-5.5-pro",
"openai_codex:gpt-5.2",
"openai_codex:gpt-5.3-codex",
"openai_codex:gpt-5.4",
"openai_codex:gpt-5.4-mini",
"openai_codex:gpt-5.5",
"openrouter:anthropic/claude-opus-4.6",
"openrouter:anthropic/claude-opus-4.7",
"openrouter:anthropic/claude-opus-4.7-fast",
"openrouter:anthropic/claude-opus-4.8",
"openrouter:anthropic/claude-sonnet-5",
"openrouter:deepseek/deepseek-v4-flash",
"openrouter:deepseek/deepseek-v4-flash:free",
"openrouter:deepseek/deepseek-v4-pro",
"openrouter:google/gemini-3.5-flash",
"openrouter:google/gemini-3.1-pro-preview",
"openrouter:moonshotai/kimi-k2.7-code",
"openrouter:nvidia/nemotron-3-ultra-550b-a55b",
"openrouter:openai/gpt-5.4",
"openrouter:openai/gpt-5.4-mini",
"openrouter:openai/gpt-5.4-pro",
"openrouter:openai/gpt-5.5",
"openrouter:openai/gpt-5.5-pro",
"openrouter:openrouter/fusion",
"openrouter:qwen/qwen3.7-plus",
"openrouter:z-ai/glm-5.2",
}
)
"""Hand-curated frontier-tier models promoted across the UI.
_RECOMMENDED_MODELS: dict[str, str] = {
"anthropic:claude-opus-4-7": "Claude Opus 4.7",
"anthropic:claude-opus-4-8": "Claude Opus 4.8",
"anthropic:claude-sonnet-5": "Claude Sonnet 5",
"baseten:deepseek-ai/DeepSeek-V4-Pro": "DeepSeek V4 Pro",
"baseten:moonshotai/Kimi-K2.7-Code": "Kimi K2.7 Code",
"baseten:nvidia/NVIDIA-Nemotron-3-Ultra-550B-A55B": "Nemotron 3 Ultra 550B A55B",
"baseten:zai-org/GLM-5.2": "GLM 5.2",
"fireworks:accounts/fireworks/models/deepseek-v4-pro": "DeepSeek V4 Pro",
"fireworks:accounts/fireworks/models/glm-5p2": "GLM 5.2",
"fireworks:accounts/fireworks/models/kimi-k2p7-code": "Kimi K2.7 Code",
"fireworks:accounts/fireworks/models/minimax-m3": "MiniMax-M3",
"fireworks:accounts/fireworks/models/qwen3p7-plus": "Qwen 3.7 Plus",
"google_genai:gemini-3.5-flash": "Gemini 3.5 Flash",
"google_genai:gemini-3.1-pro-preview": "Gemini 3.1 Pro Preview",
"ollama:deepseek-v4-flash:cloud": "DeepSeek V4 Flash",
"ollama:deepseek-v4-pro:cloud": "DeepSeek V4 Pro",
"ollama:glm-5.2:cloud": "GLM 5.2",
"ollama:kimi-k2.7-code:cloud": "Kimi K2.7 Code",
"ollama:minimax-m3:cloud": "MiniMax-M3",
"openai:gpt-5.4": "GPT-5.4",
"openai:gpt-5.4-mini": "GPT-5.4 mini",
"openai:gpt-5.4-pro": "GPT-5.4 Pro",
"openai:gpt-5.5": "GPT-5.5",
"openai:gpt-5.5-pro": "GPT-5.5 Pro",
"openai_codex:gpt-5.2": "GPT-5.2",
"openai_codex:gpt-5.3-codex": "GPT-5.3 Codex",
"openai_codex:gpt-5.4": "GPT-5.4",
"openai_codex:gpt-5.4-mini": "GPT-5.4 mini",
"openai_codex:gpt-5.5": "GPT-5.5",
"openrouter:anthropic/claude-opus-4.6": "Claude Opus 4.6",
"openrouter:anthropic/claude-opus-4.7": "Claude Opus 4.7",
"openrouter:anthropic/claude-opus-4.7-fast": "Claude Opus 4.7 Fast",
"openrouter:anthropic/claude-opus-4.8": "Claude Opus 4.8",
"openrouter:anthropic/claude-sonnet-5": "Claude Sonnet 5",
"openrouter:deepseek/deepseek-v4-flash": "DeepSeek V4 Flash",
"openrouter:deepseek/deepseek-v4-flash:free": "DeepSeek V4 Flash (free)",
"openrouter:deepseek/deepseek-v4-pro": "DeepSeek V4 Pro",
"openrouter:google/gemini-3.5-flash": "Gemini 3.5 Flash",
"openrouter:google/gemini-3.1-pro-preview": "Gemini 3.1 Pro Preview",
"openrouter:moonshotai/kimi-k2.7-code": "Kimi K2.7 Code",
"openrouter:nvidia/nemotron-3-ultra-550b-a55b": "Nemotron 3 Ultra 550B A55B",
"openrouter:openai/gpt-5.4": "GPT-5.4",
"openrouter:openai/gpt-5.4-mini": "GPT-5.4 mini",
"openrouter:openai/gpt-5.4-pro": "GPT-5.4 Pro",
"openrouter:openai/gpt-5.5": "GPT-5.5",
"openrouter:openai/gpt-5.5-pro": "GPT-5.5 Pro",
"openrouter:openrouter/fusion": "OpenRouter Fusion",
"openrouter:qwen/qwen3.7-plus": "Qwen 3.7 Plus",
"openrouter:z-ai/glm-5.2": "GLM 5.2",
}
"""Hand-curated frontier-tier models promoted across the UI, mapped to a
human-readable display name.
Used by the onboarding picker (`curated=True`) and by the in-`/model`
"Recommended only" toggle (Ctrl+R). Same model IDs may appear under multiple
providers (e.g. Kimi-K2.7-Code via `baseten`, `fireworks`, `ollama`, and
`openrouter`) and are listed under each provider intentionally so the user
can pick whichever provider they have credentials for.
"Recommended only" toggle (Ctrl+R). Membership tests and iteration operate on
the spec keys; the names are a display fallback for `_get_model_display_name`
when a provider package (and thus its profile `name`) is not installed — the
common case for uninstalled recommendations and onboarding, where the raw
model id (e.g. `accounts/fireworks/models/kimi-k2p7-code`) would otherwise
show. When a profile is available its upstream `name` wins, so these stay a
safety net rather than a second source of truth.
Same model IDs may appear under multiple providers (e.g. Kimi K2.7 Code via
`baseten`, `fireworks`, `ollama`, and `openrouter`) and are listed under each
provider intentionally so the user can pick whichever provider they have
credentials for.
"""
@@ -159,6 +167,7 @@ class ModelOption(Static):
*,
auth_status: ProviderAuthStatus | None = None,
classes: str = "",
show_provider: bool = True,
) -> None:
"""Initialize a model option.
@@ -170,10 +179,18 @@ class ModelOption(Static):
index: The index of this option in the filtered list.
auth_status: Provider auth/readiness status.
classes: CSS classes for styling.
show_provider: Whether the row appends a dim `(provider)` tag after
the model name. `True` for the cross-provider "Recent" section,
which has no provider header to disambiguate the same model
offered by multiple providers; `False` for provider-grouped
rows where the header already names the provider. Persisted on
the widget so incremental relabels in `_move_selection`
reproduce the same display.
"""
super().__init__(label, classes=classes)
self.model_spec = model_spec
self.index = index
self.show_provider = show_provider
self.auth_status = auth_status or ProviderAuthStatus(
state=ProviderAuthState.UNKNOWN,
provider=provider,
@@ -582,9 +599,9 @@ class ModelSelectorScreen(ModalScreen[tuple[str, str] | None]):
)
# Seeded from the discovered models; a recommended spec already
# surfaced here is skipped below. Recommended specs are unique (a
# frozenset iterated once), so this entry guard is the only dedup
# needed and the set never has to grow inside the loop.
# surfaced here is skipped below. Recommended specs are unique (dict
# keys iterated once), so this entry guard is the only dedup needed
# and the set never has to grow inside the loop.
existing_specs = {spec for spec, _ in all_models}
installed_recommended: list[tuple[str, str]] = []
uninstalled_recommended: list[tuple[str, str]] = []
@@ -837,11 +854,38 @@ class ModelSelectorScreen(ModalScreen[tuple[str, str] | None]):
tokens = query.split()
search_models = self._all_models if self._curated else self._unfiltered_models
# Match against what the user actually sees, not just the raw spec: the
# friendly model name and provider label are folded into the search
# haystack so e.g. "Opus 4.8" finds `anthropic:claude-opus-4-8` (whose
# spec, with hyphens, the "4.8" token can't subsequence-match) and
# "OpenAI Codex" finds the codex rows. The spec stays in the haystack so
# existing muscle-memory queries keep working.
from deepagents_code.widgets.auth import provider_display_name
config = ModelConfig.load()
provider_labels: dict[str, str] = {}
# Resolve the display labels up front, *outside* the try below. That
# fallback exists for `Matcher` choking on edge-case input; folding
# label resolution into it would misattribute an error from
# `_get_model_display_name`/`provider_display_name` to the matcher and
# silently drop the user's filter. Any failure here should surface, not
# masquerade as "no matches".
haystacks: list[tuple[str, str, str]] = []
for spec, provider in search_models:
label = provider_labels.get(provider)
if label is None:
label = provider_display_name(provider, config)
provider_labels[provider] = label
haystacks.append(
(spec, provider, f"{spec} {self._get_model_display_name(spec)} {label}")
)
try:
matchers = [Matcher(token, case_sensitive=False) for token in tokens]
scored: list[tuple[float, str, str]] = []
for spec, provider in search_models:
scores = [m.match(spec) for m in matchers]
for spec, provider, haystack in haystacks:
scores = [m.match(haystack) for m in matchers]
if all(s > 0 for s in scores):
scored.append((min(scores), spec, provider))
except Exception:
@@ -1106,9 +1150,18 @@ class ModelSelectorScreen(ModalScreen[tuple[str, str] | None]):
selected_widget = widget
flat_index += 1
# Resolve friendly provider labels via the shared helper so headers
# match the `/auth` and install UIs (e.g. `openai_codex` renders as
# "OpenAI Codex (ChatGPT login)"). Load config once; the helper reads a
# user-configured `display_name` before the built-in map.
from deepagents_code.widgets.auth import provider_display_name
config = ModelConfig.load()
for provider, model_entries in by_provider.items():
# Provider header; auth/readiness indicator appended only when non-empty.
auth_status = auth_statuses[provider]
provider_label = provider_display_name(provider, config)
if provider in self._install_extras:
auth_indicator = self._install_indicator()
else:
@@ -1116,13 +1169,13 @@ class ModelSelectorScreen(ModalScreen[tuple[str, str] | None]):
if auth_indicator:
header_content = Content.from_markup(
"[bold]$provider[/bold] [dim]$auth[/dim]",
provider=provider,
provider=provider_label,
auth=auth_indicator,
)
else:
header_content = Content.from_markup(
"[bold]$provider[/bold]",
provider=provider,
provider=provider_label,
)
all_widgets.append(Static(header_content, classes="model-provider-header"))
@@ -1137,7 +1190,11 @@ class ModelSelectorScreen(ModalScreen[tuple[str, str] | None]):
classes += " model-option-current"
label = self._build_option_label(
model_spec, provider, auth_status, selected=is_selected
model_spec,
provider,
auth_status,
selected=is_selected,
show_provider=False,
)
widget = ModelOption(
label=label,
@@ -1146,6 +1203,7 @@ class ModelSelectorScreen(ModalScreen[tuple[str, str] | None]):
index=flat_index,
auth_status=auth_status,
classes=classes,
show_provider=False,
)
all_widgets.append(widget)
self._option_widgets.append(widget)
@@ -1199,9 +1257,16 @@ class ModelSelectorScreen(ModalScreen[tuple[str, str] | None]):
auth_status: ProviderAuthStatus,
*,
selected: bool,
show_provider: bool = True,
) -> Content:
"""Build a model-option label from the current screen state.
Every row shows the model's human-readable name (via
`_get_model_display_name`). The cross-provider "Recent" section
(`show_provider=True`) additionally appends a dim `(provider)` tag,
since it has no provider header to disambiguate the same model offered
by multiple providers.
Centralizes the per-row flag derivation (current/default/status/
`install_required`) shared by the full rebuild in `_update_display`
and the incremental relabel in `_move_selection`, so the two paths
@@ -1215,10 +1280,18 @@ class ModelSelectorScreen(ModalScreen[tuple[str, str] | None]):
install-required set.
auth_status: Provider auth/readiness status for the row.
selected: Whether this row is the highlighted one.
show_provider: Whether to append the dim `(provider)` tag — `True`
for Recent rows, `False` for provider-grouped rows. The tag
uses the compact brand label (`provider_short_name`).
Returns:
Styled `Content` label.
"""
provider_label: str | None = None
if show_provider:
from deepagents_code.widgets.auth import provider_short_name
provider_label = provider_short_name(provider)
return self._format_option_label(
model_spec,
selected=selected,
@@ -1227,6 +1300,8 @@ class ModelSelectorScreen(ModalScreen[tuple[str, str] | None]):
is_default=model_spec == self._default_spec,
status=self._get_model_status(model_spec),
install_required=provider in self._install_extras,
display_name=self._get_model_display_name(model_spec),
provider_label=provider_label,
)
@staticmethod
@@ -1239,6 +1314,8 @@ class ModelSelectorScreen(ModalScreen[tuple[str, str] | None]):
is_default: bool = False,
status: str | None = None,
install_required: bool = False,
display_name: str | None = None,
provider_label: str | None = None,
) -> Content:
"""Build the display label for a model option.
@@ -1254,6 +1331,16 @@ class ModelSelectorScreen(ModalScreen[tuple[str, str] | None]):
install_required: Whether the provider's integration package is not
installed; renders the spec dimmed since selecting it prompts
an install rather than switching immediately.
display_name: Text to show in place of the full `model_spec`. When
`None`, the full spec is shown. Both the Recent and
provider-grouped rows pass the model's human-readable name (see
`_get_model_display_name`).
provider_label: When set (and `display_name` is given), appends a
dim ` (provider)` tag after the name — used by the
cross-provider Recent section, which has no provider header to
disambiguate the same model across providers. `None` for
provider-grouped rows. Ignored when `display_name` is `None`,
since the raw spec already embeds the provider.
Returns:
Styled Content label.
@@ -1261,19 +1348,28 @@ class ModelSelectorScreen(ModalScreen[tuple[str, str] | None]):
colors = theme.get_theme_colors()
glyphs = get_glyphs()
cursor = f"{glyphs.cursor} " if selected else " "
display = model_spec if display_name is None else display_name
# When selected, skip the inline primary color — CSS already flips the
# row to ($primary bg, $background fg). Keep `bold` so the default
# emphasis survives both states.
if install_required and not selected:
spec = Content.styled(model_spec, "dim")
spec = Content.styled(display, "dim")
elif auth_status.blocks_start:
spec = Content.styled(model_spec, colors.warning)
spec = Content.styled(display, colors.warning)
elif is_default and selected:
spec = Content.styled(model_spec, "bold")
spec = Content.styled(display, "bold")
elif is_default:
spec = Content.styled(model_spec, f"bold {colors.primary}")
spec = Content.styled(display, f"bold {colors.primary}")
else:
spec = Content(model_spec)
spec = Content(display)
# Dim provider tag disambiguates Recent rows (no provider header).
# Styled like `(current)` — always dim, so it survives row selection.
# Requires a friendly `display_name`: tagging the raw spec (which
# already embeds the provider) would print the provider twice.
if provider_label and display_name is not None:
provider_tag = Content.styled(f" ({provider_label})", "dim")
else:
provider_tag = Content("")
suffix = Content.styled(" (current)", "dim") if current else Content("")
if is_default and selected:
default_suffix = Content.styled(" (default)", "bold")
@@ -1287,7 +1383,9 @@ class ModelSelectorScreen(ModalScreen[tuple[str, str] | None]):
status_suffix = Content.styled(f" ({status})", colors.warning)
else:
status_suffix = Content("")
return Content.assemble(cursor, spec, suffix, default_suffix, status_suffix)
return Content.assemble(
cursor, spec, provider_tag, suffix, default_suffix, status_suffix
)
@staticmethod
def _format_footer(
@@ -1422,6 +1520,40 @@ class ModelSelectorScreen(ModalScreen[tuple[str, str] | None]):
return None
return profile.get("status")
def _get_model_display_name(self, model_spec: str) -> str:
"""Resolve the friendly display name for a model spec.
Used by every row (provider-grouped and the cross-provider Recent
section) and folded into the search haystack. Prefers the profile's
human-readable `name` (e.g. `'Claude Sonnet 5'`), which reads better
than the raw model id. When no profile is loaded — the case for
uninstalled recommendations and onboarding — falls back to the
hardcoded name in `_RECOMMENDED_MODELS`, then the model portion of the
spec, then the spec itself.
Args:
model_spec: The `provider:model` string.
Returns:
The display name for the row.
"""
entry = self._profiles.get(model_spec)
if entry:
profile = entry.get("profile")
# `profile` originates from provider packages, so guard its type
# rather than trusting the schema before `.get`.
if isinstance(profile, dict):
name = profile.get("name")
if isinstance(name, str) and name:
return name
recommended = _RECOMMENDED_MODELS.get(model_spec)
if recommended:
return recommended
parsed = ModelSpec.try_parse(model_spec)
# `parsed.model` can be empty for a malformed spec like `provider:`;
# fall back to the raw spec rather than rendering a blank row.
return parsed.model if parsed and parsed.model else model_spec
def _update_footer(self) -> None:
"""Update the detail footer for the currently highlighted model."""
footer = self.query_one("#model-detail-footer", Static)
@@ -1461,6 +1593,7 @@ class ModelSelectorScreen(ModalScreen[tuple[str, str] | None]):
old_widget.provider,
old_widget.auth_status,
selected=False,
show_provider=old_widget.show_provider,
)
)
@@ -1473,6 +1606,7 @@ class ModelSelectorScreen(ModalScreen[tuple[str, str] | None]):
new_widget.provider,
new_widget.auth_status,
selected=True,
show_provider=new_widget.show_provider,
)
)
@@ -21,7 +21,8 @@ from deepagents_code.widgets.auth import (
AuthPromptScreen,
AuthResult,
_is_safe_acquisition_url,
_provider_display_name,
provider_display_name,
provider_short_name,
)
from deepagents_code.widgets.codex_auth import CodexAuthScreen
@@ -203,23 +204,46 @@ class TestAuthPromptScreen:
def test_display_name_falls_back_to_title_cased_key(self) -> None:
"""Unmapped provider keys degrade to a readable title-cased label."""
assert _provider_display_name("acme_gateway") == "Acme Gateway"
assert provider_display_name("acme_gateway") == "Acme Gateway"
def test_config_display_name_overrides_builtin_map(self) -> None:
"""A configured `display_name` wins over the built-in label.
Pins the resolution order (config > built-in map) so reordering the
`or` chain in `_provider_display_name` would fail a test instead of
`or` chain in `provider_display_name` would fail a test instead of
silently shadowing user config.
"""
config = model_config.ModelConfig(
providers={"openai": {"display_name": "Custom OpenAI"}}
)
assert _provider_display_name("openai", config) == "Custom OpenAI"
assert provider_display_name("openai", config) == "Custom OpenAI"
# Without the override, resolution falls through to the built-in map.
empty = model_config.ModelConfig()
builtin_label = PROVIDER_DISPLAY_NAMES["openai"]
assert _provider_display_name("openai", empty) == builtin_label
assert provider_display_name("openai", empty) == builtin_label
def test_short_name_uses_builtin_brand_map(self) -> None:
"""A provider with a verbose display name gets its compact brand."""
empty = model_config.ModelConfig()
assert provider_short_name("openai_codex", empty) == "OpenAI Codex"
def test_short_name_falls_back_to_display_name(self) -> None:
"""Providers without a brand override reuse the display name."""
empty = model_config.ModelConfig()
assert provider_short_name("openai", empty) == PROVIDER_DISPLAY_NAMES["openai"]
# Unmapped keys degrade through display-name resolution to title case.
assert provider_short_name("acme_gateway", empty) == "Acme Gateway"
def test_config_short_name_overrides_builtin_map(self) -> None:
"""A configured `short_name` wins over the built-in brand map.
Pins the resolution order (config > built-in map > display name) so
reordering the `or` chain in `provider_short_name` fails a test.
"""
config = model_config.ModelConfig(
providers={"openai_codex": {"short_name": "Codex"}}
)
assert provider_short_name("openai_codex", config) == "Codex"
def test_is_safe_acquisition_url_rejects_non_http_schemes(self) -> None:
"""Only http/https URLs are eligible to render as clickable links."""
@@ -1507,6 +1507,7 @@ api_key_env = "OPENAI_API_KEY"
config_path.write_text("""
[models.providers.my_gateway]
display_name = "My Gateway"
short_name = "Gateway"
api_key_url = "https://gateway.example/keys"
models = ["my-model"]
api_key_env = "MY_GATEWAY_API_KEY"
@@ -1514,11 +1515,13 @@ api_key_env = "MY_GATEWAY_API_KEY"
config = ModelConfig.load(config_path)
assert config.get_provider_display_name("my_gateway") == "My Gateway"
assert config.get_provider_short_name("my_gateway") == "Gateway"
assert (
config.get_provider_api_key_url("my_gateway")
== "https://gateway.example/keys"
)
assert config.get_provider_display_name("missing") is None
assert config.get_provider_short_name("missing") is None
assert config.get_provider_api_key_url("missing") is None
def test_ignores_non_string_provider_api_key_url(self, tmp_path):
@@ -1547,8 +1550,21 @@ api_key_env = "MY_GATEWAY_API_KEY"
assert config.get_provider_display_name("my_gateway") is None
def test_ignores_non_string_provider_short_name(self, tmp_path):
"""Non-string provider short names fall back to the display name."""
config_path = tmp_path / "config.toml"
config_path.write_text("""
[models.providers.my_gateway]
short_name = 123
models = ["my-model"]
api_key_env = "MY_GATEWAY_API_KEY"
""")
config = ModelConfig.load(config_path)
assert config.get_provider_short_name("my_gateway") is None
def test_warns_on_non_string_provider_metadata(self, tmp_path, caplog):
"""Malformed `display_name`/`api_key_url` warn at load, matching siblings.
"""Malformed `display_name`/`short_name`/`api_key_url` warn at load.
Surfaces the misconfiguration as a diagnostic instead of silently
dropping it, consistent with the `enabled`/`class_path` validation.
@@ -1557,6 +1573,7 @@ api_key_env = "MY_GATEWAY_API_KEY"
config_path.write_text("""
[models.providers.my_gateway]
display_name = 123
short_name = 456
api_key_url = ["not", "a", "string"]
models = ["my-model"]
api_key_env = "MY_GATEWAY_API_KEY"
@@ -1566,6 +1583,7 @@ api_key_env = "MY_GATEWAY_API_KEY"
messages = [r.getMessage() for r in caplog.records]
assert any("non-string 'display_name'" in m for m in messages)
assert any("non-string 'short_name'" in m for m in messages)
assert any("non-string 'api_key_url'" in m for m in messages)
def test_loads_custom_base_url(self, tmp_path):
@@ -1,6 +1,6 @@
"""Tests for ModelSelectorScreen."""
from collections.abc import Callable
from collections.abc import Callable, Iterator
from pathlib import Path
from typing import Any, ClassVar
from unittest.mock import MagicMock
@@ -23,13 +23,22 @@ from deepagents_code.widgets.model_selector import ModelSelectorScreen
@pytest.fixture(autouse=True)
def _seed_provider_credentials(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
def _seed_provider_credentials(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> Iterator[None]:
"""Seed credentials so dismissal tests aren't blocked by missing keys.
The selector now opens an auth prompt when the highlighted provider
has no key. Most tests in this file just want to assert dismissal
behavior, so we seed env vars for the providers their fixtures use
and redirect the credential store into a clean temp dir.
Also redirects `DEFAULT_CONFIG_PATH` to a (nonexistent) temp file and
clears the process-wide config cache. The selector now resolves provider
labels via `ModelConfig.load()`, which defaults to the developer's real
`~/.deepagents/config.toml`; a local `display_name`/`short_name` override
would otherwise flip label assertions. This keeps the suite hermetic.
"""
monkeypatch.setenv("ANTHROPIC_API_KEY", "test-key")
monkeypatch.setenv("OPENAI_API_KEY", "test-key")
@@ -40,6 +49,15 @@ def _seed_provider_credentials(monkeypatch: pytest.MonkeyPatch, tmp_path: Path)
monkeypatch.setattr(
"deepagents_code.model_config.DEFAULT_STATE_DIR", tmp_path / ".state"
)
from deepagents_code import model_config
monkeypatch.setattr(model_config, "DEFAULT_CONFIG_PATH", tmp_path / "config.toml")
# The cache is a module global that monkeypatch can't undo; clear it before
# and after so neither a prior test's real config nor this fixture's empty
# config leaks across tests.
model_config.clear_caches()
yield
model_config.clear_caches()
_FILTER_TEST_MODELS: list[tuple[str, str]] = [
@@ -548,6 +566,94 @@ class TestRecentModelsSection:
]
assert not any("Recent" in h for h in headers)
async def test_provider_header_uses_friendly_name(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Provider headers render the friendly label, not the raw config key."""
from deepagents_code.widgets import model_selector
monkeypatch.setattr(
model_selector,
"get_available_models",
lambda: {"openai_codex": ["gpt-5.5"]},
)
monkeypatch.setattr(model_selector, "load_recent_models", list)
app = ModelSelectorTestApp()
async with app.run_test() as pilot:
screen = ModelSelectorScreen()
app.push_screen(screen)
await pilot.pause()
headers = [
str(h.content)
for h in screen.query(".model-provider-header").results(Static)
]
assert any("OpenAI Codex (ChatGPT login)" in h for h in headers)
assert not any("openai_codex" in h for h in headers)
async def test_recent_row_shows_name_and_provider_tag(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Recent rows show the friendly name plus a brand `(provider)` tag."""
from deepagents_code.widgets import model_selector
monkeypatch.setattr(
model_selector,
"get_available_models",
lambda: {"openai": ["gpt-5.5"]},
)
monkeypatch.setattr(
model_selector,
"load_recent_models",
lambda: ["openai:gpt-5.5"],
)
app = ModelSelectorTestApp()
async with app.run_test() as pilot:
screen = ModelSelectorScreen()
app.push_screen(screen)
await pilot.pause()
recent = screen._option_widgets[0]
assert recent.model_spec == "openai:gpt-5.5"
assert recent.show_provider
text = str(recent.content)
assert "GPT-5.5" in text
# No brand override for openai: falls back to the display name.
assert "(OpenAI)" in text
assert "openai:gpt-5.5" not in text
async def test_recent_row_uses_short_brand_over_verbose_display_name(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
"""The Recent tag uses the compact brand, not the verbose auth label."""
from deepagents_code.widgets import model_selector
monkeypatch.setattr(
model_selector,
"get_available_models",
lambda: {"openai_codex": ["gpt-5.5"]},
)
monkeypatch.setattr(
model_selector,
"load_recent_models",
lambda: ["openai_codex:gpt-5.5"],
)
app = ModelSelectorTestApp()
async with app.run_test() as pilot:
screen = ModelSelectorScreen()
app.push_screen(screen)
await pilot.pause()
recent = screen._option_widgets[0]
assert recent.model_spec == "openai_codex:gpt-5.5"
text = str(recent.content)
assert "(OpenAI Codex)" in text
# The verbose auth label must not leak into the compact tag.
assert "ChatGPT login" not in text
async def test_recent_entries_appear_in_provider_section_too(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
@@ -1224,6 +1330,69 @@ class TestModelSelectorFuzzyMatching:
f"'claude sonnet' should match claude-sonnet models. Got: {specs}"
)
def test_fuzzy_matches_friendly_name_dotted_version(self) -> None:
"""A dotted version in the friendly name matches where the spec can't.
`anthropic:claude-opus-4-7` hyphenates its version, so the "4.7" token
cannot subsequence-match the spec. The friendly name "Claude Opus 4.7"
(from the recommended list) supplies the dotted form.
"""
screen = _model_selector_for_filtering()
screen._filter_text = "opus 4.7"
screen._update_filtered_list()
specs = [spec for spec, _ in screen._filtered_models]
assert "anthropic:claude-opus-4-7" in specs, (
f"friendly name should let 'opus 4.7' match. Got: {specs}"
)
def test_fuzzy_dotted_version_needs_friendly_name(self) -> None:
"""Negative control: without the friendly name, "4.7" can't match the spec.
Pins that the previous test passes because of the folded-in name, not
some incidental spec match — guarding the friendly-name search feature.
"""
screen = _model_selector_for_filtering()
# Neutralize the friendly name (return the hyphenated model portion, as
# the raw spec already carries) so "4.7" has no dotted form to match.
screen._get_model_display_name = ( # ty: ignore[invalid-assignment]
lambda spec: spec.split(":", 1)[-1]
)
screen._filter_text = "opus 4.7"
screen._update_filtered_list()
specs = [spec for spec, _ in screen._filtered_models]
assert "anthropic:claude-opus-4-7" not in specs
def test_fuzzy_matches_provider_friendly_label(self) -> None:
"""The provider display label — not just the key — is searchable.
Searches "chatgpt", which appears in neither the spec
(`openai_codex:gpt-5.2`), the friendly model name ("GPT-5.2"), nor the
provider key (`openai_codex`) — only in the resolved display label
"OpenAI Codex (ChatGPT login)". So a match proves the provider-label
branch of the haystack is doing the work; there is no other source for
it. Guards against the label term being silently dropped.
"""
screen = _model_selector_for_filtering()
# Inject a codex row locally rather than mutate the shared fixture that
# other filter tests count on.
codex = ("openai_codex:gpt-5.2", "openai_codex")
for models in (
screen._unfiltered_models,
screen._all_models,
screen._filtered_models,
):
models.append(codex)
screen._filter_text = "chatgpt"
screen._update_filtered_list()
specs = [spec for spec, _ in screen._filtered_models]
assert specs == ["openai_codex:gpt-5.2"], (
f"'chatgpt' should match only via the provider label. Got: {specs}"
)
async def test_tab_noop_when_no_matches(self) -> None:
"""Tab should do nothing when filter matches no models."""
app = ModelSelectorTestApp()
@@ -1845,6 +2014,70 @@ class TestFormatOptionLabel:
# Not dimmed when selected; the missing-creds warning color applies.
assert DARK_COLORS.warning in label.markup
def test_uses_display_name_when_provided(self) -> None:
"""Provider-grouped rows render the profile name, not the full spec."""
label = ModelSelectorScreen._format_option_label(
"anthropic:claude-sonnet-4-5",
selected=False,
current=False,
auth_status=ProviderAuthStatus(
state=ProviderAuthState.CONFIGURED,
provider="anthropic",
source=ProviderAuthSource.ENV,
),
display_name="Claude Sonnet 4.5",
)
assert "Claude Sonnet 4.5" in label.plain
assert "anthropic:claude-sonnet-4-5" not in label.plain
def test_shows_full_spec_when_no_display_name(self) -> None:
"""With no display_name the full spec is shown (static-method default)."""
label = ModelSelectorScreen._format_option_label(
"anthropic:claude-sonnet-4-5",
selected=False,
current=False,
auth_status=ProviderAuthStatus(
state=ProviderAuthState.CONFIGURED,
provider="anthropic",
source=ProviderAuthSource.ENV,
),
)
assert "anthropic:claude-sonnet-4-5" in label.plain
def test_appends_provider_tag_for_recent(self) -> None:
"""Recent rows show the name plus a `(provider)` tag, not the raw spec."""
label = ModelSelectorScreen._format_option_label(
"openai:gpt-5.5",
selected=False,
current=False,
auth_status=ProviderAuthStatus(
state=ProviderAuthState.CONFIGURED,
provider="openai",
source=ProviderAuthSource.ENV,
),
display_name="GPT-5.5",
provider_label="openai",
)
assert "GPT-5.5" in label.plain
assert "(openai)" in label.plain
assert "openai:gpt-5.5" not in label.plain
def test_no_provider_tag_without_label(self) -> None:
"""Provider-grouped rows omit the `(provider)` tag."""
label = ModelSelectorScreen._format_option_label(
"openai:gpt-5.5",
selected=False,
current=False,
auth_status=ProviderAuthStatus(
state=ProviderAuthState.CONFIGURED,
provider="openai",
source=ProviderAuthSource.ENV,
),
display_name="GPT-5.5",
)
assert "GPT-5.5" in label.plain
assert "(openai)" not in label.plain
class TestFormatAuthIndicator:
"""Tests for provider auth indicator labels."""
@@ -1987,6 +2220,101 @@ class TestGetModelStatus:
assert screen._get_model_status("anthropic:model") is None
class TestGetModelDisplayName:
"""Tests for _get_model_display_name resolution."""
def test_returns_profile_name_when_present(self) -> None:
"""The human-readable profile name wins over the raw model id."""
screen = ModelSelectorScreen.__new__(ModelSelectorScreen)
screen._profiles = {
"anthropic:claude-sonnet-4-5": ModelProfileEntry(
profile={"name": "Claude Sonnet 4.5"},
overridden_keys=frozenset(),
),
}
assert (
screen._get_model_display_name("anthropic:claude-sonnet-4-5")
== "Claude Sonnet 4.5"
)
def test_falls_back_to_model_id_when_no_name(self) -> None:
"""A profile without a `name` key falls back to the model portion."""
screen = ModelSelectorScreen.__new__(ModelSelectorScreen)
screen._profiles = {
"anthropic:claude-sonnet-4-5": ModelProfileEntry(
profile={"max_input_tokens": 200000},
overridden_keys=frozenset(),
),
}
assert (
screen._get_model_display_name("anthropic:claude-sonnet-4-5")
== "claude-sonnet-4-5"
)
def test_uses_recommended_name_when_no_profile(self) -> None:
"""Uninstalled recommendations use the hardcoded name, not the raw id."""
from deepagents_code.widgets import model_selector
screen = ModelSelectorScreen.__new__(ModelSelectorScreen)
screen._profiles = {}
spec = "fireworks:accounts/fireworks/models/kimi-k2p7-code"
assert spec in model_selector._RECOMMENDED_MODELS
assert screen._get_model_display_name(spec) == "Kimi K2.7 Code"
def test_profile_name_wins_over_recommended_name(self) -> None:
"""A loaded profile's `name` takes precedence over the hardcoded one."""
from deepagents_code.widgets import model_selector
spec = "openai:gpt-5.5"
assert spec in model_selector._RECOMMENDED_MODELS
screen = ModelSelectorScreen.__new__(ModelSelectorScreen)
screen._profiles = {
spec: ModelProfileEntry(
profile={"name": "GPT-5.5 (from profile)"},
overridden_keys=frozenset(),
),
}
assert screen._get_model_display_name(spec) == "GPT-5.5 (from profile)"
def test_falls_back_to_model_id_when_not_recommended(self) -> None:
"""A non-recommended spec absent from profiles falls back to the model."""
screen = ModelSelectorScreen.__new__(ModelSelectorScreen)
screen._profiles = {}
assert (
screen._get_model_display_name("openai:some-unlisted-model")
== "some-unlisted-model"
)
def test_ignores_empty_name(self) -> None:
"""An empty `name` string falls back rather than rendering blank."""
screen = ModelSelectorScreen.__new__(ModelSelectorScreen)
screen._profiles = {
"openai:some-unlisted-model": ModelProfileEntry(
profile={"name": ""},
overridden_keys=frozenset(),
),
}
assert (
screen._get_model_display_name("openai:some-unlisted-model")
== "some-unlisted-model"
)
def test_preserves_colon_in_model_id_fallback(self) -> None:
"""Only the leading `provider:` is stripped in the fallback path."""
screen = ModelSelectorScreen.__new__(ModelSelectorScreen)
screen._profiles = {}
assert (
screen._get_model_display_name("ollama:some-model:cloud")
== "some-model:cloud"
)
def test_returns_bare_spec_unchanged(self) -> None:
"""A spec without a `provider:` prefix is returned as-is."""
screen = ModelSelectorScreen.__new__(ModelSelectorScreen)
screen._profiles = {}
assert screen._get_model_display_name("gpt-5.5") == "gpt-5.5"
class TestModelDetailFooter:
"""Tests for the model detail footer in the selector."""
@@ -2289,7 +2617,7 @@ class TestModelSelectorInstallRouting:
monkeypatch.setattr(
model_selector,
"_RECOMMENDED_MODELS",
frozenset({installed_spec, uninstalled_spec}),
{installed_spec: "GLM 5.2", uninstalled_spec: "DeepSeek V4 Pro"},
)
monkeypatch.setattr(
model_selector,
@@ -2744,7 +3072,17 @@ enabled = false
# Recents render first; index 0 is the Recent-section install row.
recent_install = screen._option_widgets[0]
assert recent_install.model_spec == install_spec
assert "dim" in recent_install.content.markup
# Check the model NAME's dim specifically: the Recent row always
# carries a dim `(Baseten)` provider tag, so a bare "dim" substring
# search can't tell install-required dimming from the tag. The name
# is wrapped in `[dim]...` only when install-required and unselected.
name_dim = "[dim]Kimi K2.7 Code"
# The provider tag disambiguates the cross-provider Recent row and
# must survive `_move_selection`'s incremental relabel — which
# re-derives the label from the widget's persisted `show_provider`.
provider_tag = "(Baseten)"
assert name_dim in recent_install.content.markup
assert provider_tag in recent_install.content.markup
# `_update_display` keeps the openai row highlighted (rendered
# order: recent install, openai, provider-group install).
assert screen._selected_index == 1
@@ -2753,9 +3091,12 @@ enabled = false
screen._move_selection(-1)
await pilot.pause()
assert screen._selected_index == 0
assert "dim" not in recent_install.content.markup
assert name_dim not in recent_install.content.markup
# The tag persists regardless of selection (it's the relabel path).
assert provider_tag in recent_install.content.markup
screen._move_selection(1)
await pilot.pause()
assert screen._selected_index == 1
assert "dim" in recent_install.content.markup
assert name_dim in recent_install.content.markup
assert provider_tag in recent_install.content.markup