diff --git a/desktop/src/features/agents/AGENTS.md b/desktop/src/features/agents/AGENTS.md index 35ad4a63af..c69034746d 100644 --- a/desktop/src/features/agents/AGENTS.md +++ b/desktop/src/features/agents/AGENTS.md @@ -114,6 +114,11 @@ with a TypeScript lookup table or an id comparison in a component. published or removed. A queued update must stay visibly queued, and the catalog itself must render only relay-confirmed publications — never an optimistic local persona. +11. **Model IDs are runtime values; model labels are presentation.** Preserve + the exact discovered or configured ID when selecting and persisting a + model. Any user-facing Databricks gateway model name must go through + `lib/formatAgentModelLabel.ts` so its `databricks-` endpoint prefix is + removed consistently without changing the value sent to the runtime. ## The tests that enforce this diff --git a/desktop/src/features/agents/lib/agentCardModelLabel.test.mjs b/desktop/src/features/agents/lib/agentCardModelLabel.test.mjs index 696a055d20..87aea9cd72 100644 --- a/desktop/src/features/agents/lib/agentCardModelLabel.test.mjs +++ b/desktop/src/features/agents/lib/agentCardModelLabel.test.mjs @@ -56,3 +56,21 @@ test("resolveAgentCardModelLabel — non-inherited agent with a blank resolved m }); assert.equal(label, "Default model (claude-sonnet)"); }); + +test("resolveAgentCardModelLabel — Databricks endpoint IDs render without the gateway prefix", () => { + const label = resolveAgentCardModelLabel({ + agent: { modelSource: "definition", model: "databricks-claude-opus-4-7" }, + personaModel: null, + defaultModel: "databricks-gpt-5-5", + }); + assert.equal(label, "Claude Opus 4.7"); +}); + +test("resolveAgentCardModelLabel — inherited Databricks models render a clean name", () => { + const label = resolveAgentCardModelLabel({ + agent: undefined, + personaModel: null, + defaultModel: "databricks-gpt-5-5", + }); + assert.equal(label, "Default model (GPT-5.5)"); +}); diff --git a/desktop/src/features/agents/lib/agentCardModelLabel.ts b/desktop/src/features/agents/lib/agentCardModelLabel.ts index 4ec06f10c5..cef9196793 100644 --- a/desktop/src/features/agents/lib/agentCardModelLabel.ts +++ b/desktop/src/features/agents/lib/agentCardModelLabel.ts @@ -1,4 +1,7 @@ -import { formatAgentModelLabel } from "./formatAgentModelLabel"; +import { + formatAgentModelLabel, + formatModelDisplayName, +} from "./formatAgentModelLabel"; import type { ManagedAgent } from "@/shared/api/types"; /** @@ -39,6 +42,6 @@ export function resolveAgentCardModelLabel(input: { } export function formatDefaultModelLabel(defaultModel: string) { - const model = defaultModel.trim(); + const model = formatModelDisplayName(defaultModel); return model ? `Default model (${model})` : "Default model"; } diff --git a/desktop/src/features/agents/lib/formatAgentModelLabel.ts b/desktop/src/features/agents/lib/formatAgentModelLabel.ts index 6c32d53937..63f30f652d 100644 --- a/desktop/src/features/agents/lib/formatAgentModelLabel.ts +++ b/desktop/src/features/agents/lib/formatAgentModelLabel.ts @@ -1,8 +1,65 @@ +const DATABRICKS_PREFIX = "databricks-"; + +const MODEL_WORD_LABELS: Readonly> = { + bge: "BGE", + claude: "Claude", + en: "EN", + glm: "GLM", + gpt: "GPT", + gte: "GTE", + llama: "Llama", + meta: "Meta", + mlflow: "MLflow", + openai: "OpenAI", +}; + +/** + * Turns a Databricks gateway endpoint into its human-facing model name while + * preserving the endpoint ID everywhere it is sent to the runtime. + * + * Examples: + * - `databricks-claude-opus-4-7` → `Claude Opus 4.7` + * - `databricks-gpt-5-5` → `GPT-5.5` + */ +export function formatModelDisplayName(model: string | null | undefined) { + const trimmed = model?.trim(); + if (!trimmed) return ""; + + if (!trimmed.toLowerCase().startsWith(DATABRICKS_PREFIX)) { + return trimmed; + } + + const parts = trimmed.slice(DATABRICKS_PREFIX.length).split("-"); + const labelParts: string[] = []; + for (let index = 0; index < parts.length; index += 1) { + const part = parts[index]; + const nextPart = parts[index + 1]; + + if (/^\d+$/.test(part) && nextPart && /^\d+$/.test(nextPart)) { + labelParts.push(`${part}.${nextPart}`); + index += 1; + continue; + } + + if (/^\d+b$/i.test(part)) { + labelParts.push(part.toUpperCase()); + continue; + } + + labelParts.push( + MODEL_WORD_LABELS[part.toLowerCase()] ?? + `${part.slice(0, 1).toUpperCase()}${part.slice(1)}`, + ); + } + + const label = labelParts.join(" "); + return label.replace(/^GPT (\d)/, "GPT-$1"); +} + /** * Returns a human-readable model label for an agent or persona, falling back to * "Auto" when no model is set (empty or whitespace-only). */ export function formatAgentModelLabel(model: string | null | undefined) { - const trimmed = model?.trim(); - return trimmed && trimmed.length > 0 ? trimmed : "Auto"; + return formatModelDisplayName(model) || "Auto"; } diff --git a/desktop/src/features/agents/ui/ManagedAgentRow.tsx b/desktop/src/features/agents/ui/ManagedAgentRow.tsx index 606d2b7883..f559503b94 100644 --- a/desktop/src/features/agents/ui/ManagedAgentRow.tsx +++ b/desktop/src/features/agents/ui/ManagedAgentRow.tsx @@ -24,6 +24,7 @@ import { cn } from "@/shared/lib/cn"; import { Button } from "@/shared/ui/button"; import { AgentConfigPanel } from "./AgentConfigPanel"; import { friendlyAgentLastError } from "@/features/agents/lib/friendlyAgentLastError"; +import { formatModelDisplayName } from "@/features/agents/lib/formatAgentModelLabel"; import { ManagedAgentLogPanel } from "./ManagedAgentLogPanel"; import { PubKey } from "@/shared/ui/PubKey"; import { SubsectionLabel } from "@/shared/ui/PageHeader"; @@ -410,7 +411,9 @@ function RuntimeBlock({ {runtimeSource || agent.model ? (
{runtimeSource ? {runtimeSource} : null} - {agent.model ? {agent.model} : null} + {agent.model ? ( + {formatModelDisplayName(agent.model)} + ) : null}
) : null} diff --git a/desktop/src/features/agents/ui/ModelPicker.tsx b/desktop/src/features/agents/ui/ModelPicker.tsx index f7bafde99b..3ddcedb9ee 100644 --- a/desktop/src/features/agents/ui/ModelPicker.tsx +++ b/desktop/src/features/agents/ui/ModelPicker.tsx @@ -9,6 +9,7 @@ import type { AgentModelsResponse, ManagedAgent } from "@/shared/api/types"; import { getAgentModels, updateManagedAgent } from "@/shared/api/tauri"; import { switchManagedAgentModel } from "@/shared/api/agentControl"; import { awaitLiveSwitchOutcome } from "@/features/agents/lib/liveSwitchOutcome"; +import { formatModelDisplayName } from "@/features/agents/lib/formatAgentModelLabel"; import { subscribeControlResults } from "@/features/agents/observerRelayStore"; import { useActiveAgentTurns } from "@/features/agents/activeAgentTurnsStore"; import { @@ -83,9 +84,9 @@ export function ModelPicker({ const currentValue = agent.model ?? modelsData?.agentDefaultModel ?? ""; const displayLabel = - agent.model ?? + formatModelDisplayName(agent.model) || (modelsData?.agentDefaultModel - ? `${modelsData.agentDefaultModel} (default)` + ? `${formatModelDisplayName(modelsData.agentDefaultModel)} (default)` : hasRequestedModels && loading ? "Loading..." : "Auto"); @@ -221,7 +222,9 @@ export function ModelPicker({
{agent.model ? ( <> -

{agent.model}

+

+ {formatModelDisplayName(agent.model)} +

This runtime does not support switching models.

@@ -237,7 +240,7 @@ export function ModelPicker({ > {modelsData.models.map((model) => ( - {model.name ?? model.id} + {formatModelDisplayName(model.name ?? model.id)} ))} diff --git a/desktop/src/features/agents/ui/agentConfigOptions.tsx b/desktop/src/features/agents/ui/agentConfigOptions.tsx index 1313d2cec4..fef77585cb 100644 --- a/desktop/src/features/agents/ui/agentConfigOptions.tsx +++ b/desktop/src/features/agents/ui/agentConfigOptions.tsx @@ -2,6 +2,7 @@ import type { AcpRuntimeCatalogEntry, GlobalAgentConfig, } from "@/shared/api/types"; +import { formatModelDisplayName } from "../lib/formatAgentModelLabel"; import { BUZZ_AGENT_THINKING_EFFORT } from "./buzzAgentConfig"; import type { RuntimeFileConfigSubset } from "@/shared/api/tauri"; // Dialogs import getDefaultPersonaRuntime via this re-export; lib code imports @@ -307,7 +308,7 @@ export function getDefaultLlmProviderLabel( * Otherwise falls back to the generic `"Default model"` placeholder. */ export function getDefaultLlmModelLabel(globalModel?: string) { - const trimmedGlobal = (globalModel ?? "").trim(); + const trimmedGlobal = formatModelDisplayName(globalModel); return trimmedGlobal ? `Use agent defaults (${trimmedGlobal})` : "Default model"; diff --git a/desktop/src/features/agents/ui/usePersonaModelDiscovery.test.mjs b/desktop/src/features/agents/ui/usePersonaModelDiscovery.test.mjs index ecb36a6fc5..7832f97238 100644 --- a/desktop/src/features/agents/ui/usePersonaModelDiscovery.test.mjs +++ b/desktop/src/features/agents/ui/usePersonaModelDiscovery.test.mjs @@ -61,6 +61,27 @@ test("default row shows the harness-reported current model when available", () = ); }); +test("Databricks endpoint IDs keep their value while using a clean display label", () => { + const options = getDiscoveredPersonaModelOptions( + response({ + agentDefaultModel: "databricks-gpt-5-5", + models: [ + { + id: "databricks-claude-opus-4-7", + name: "databricks-claude-opus-4-7", + description: null, + }, + ], + }), + "databricks_v2", + ); + + assert.deepEqual(options, [ + { id: "", label: "Default model (GPT-5.5)" }, + { id: "databricks-claude-opus-4-7", label: "Claude Opus 4.7" }, + ]); +}); + test("the 'default' id match is case-insensitive and trimmed", () => { const options = getDiscoveredPersonaModelOptions( response({ diff --git a/desktop/src/features/agents/ui/usePersonaModelDiscovery.ts b/desktop/src/features/agents/ui/usePersonaModelDiscovery.ts index e7b434288f..abaa82425a 100644 --- a/desktop/src/features/agents/ui/usePersonaModelDiscovery.ts +++ b/desktop/src/features/agents/ui/usePersonaModelDiscovery.ts @@ -10,6 +10,7 @@ import { formatModelDiscoveryErrorStatus, type PersonaModelDiscoveryStatus, } from "./personaModelDiscoveryStatus"; +import { formatModelDisplayName } from "../lib/formatAgentModelLabel"; import type { PersonaModelOption } from "./agentConfigOptions"; import { providerRequiresExplicitModel } from "./agentConfigOptions"; @@ -64,7 +65,7 @@ export function getDiscoveredPersonaModelOptions( provider === "relay-mesh" ? "Default (auto)" : agentDefaultModel - ? `Default model (${agentDefaultModel})` + ? `Default model (${formatModelDisplayName(agentDefaultModel)})` : "Default model", }, ]; @@ -77,7 +78,7 @@ export function getDiscoveredPersonaModelOptions( ...defaultModelOption, ...explicitModels.map((model) => ({ id: model.id, - label: model.name?.trim() || model.id, + label: formatModelDisplayName(model.name?.trim() || model.id), })), ]; } diff --git a/desktop/src/features/profile/ui/UserProfilePopover.tsx b/desktop/src/features/profile/ui/UserProfilePopover.tsx index f2739088a6..a13ea79db7 100644 --- a/desktop/src/features/profile/ui/UserProfilePopover.tsx +++ b/desktop/src/features/profile/ui/UserProfilePopover.tsx @@ -24,6 +24,7 @@ import { import { useIsManagedAgent } from "@/features/agent-memory/hooks"; import { useIdentityQuery } from "@/shared/api/hooks"; import { useAgentWorking } from "@/features/agents/agentWorkingSignal"; +import { formatModelDisplayName } from "@/features/agents/lib/formatAgentModelLabel"; import { formatOwnerLabel, ownsAuthorAgent, @@ -611,7 +612,9 @@ export function UserProfilePopover({ {runtimeLabel(relayAgent.agentType)} ) : null} {managedAgent?.model ? ( - {managedAgent.model} + + {formatModelDisplayName(managedAgent.model)} + ) : null} {managedAgent?.acpCommand ? ( ACP: {managedAgent.acpCommand}