Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions desktop/src/features/agents/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
18 changes: 18 additions & 0 deletions desktop/src/features/agents/lib/agentCardModelLabel.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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)");
});
7 changes: 5 additions & 2 deletions desktop/src/features/agents/lib/agentCardModelLabel.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { formatAgentModelLabel } from "./formatAgentModelLabel";
import {
formatAgentModelLabel,
formatModelDisplayName,
} from "./formatAgentModelLabel";
import type { ManagedAgent } from "@/shared/api/types";

/**
Expand Down Expand Up @@ -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";
}
61 changes: 59 additions & 2 deletions desktop/src/features/agents/lib/formatAgentModelLabel.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,65 @@
const DATABRICKS_PREFIX = "databricks-";

const MODEL_WORD_LABELS: Readonly<Record<string, string>> = {
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";
}
5 changes: 4 additions & 1 deletion desktop/src/features/agents/ui/ManagedAgentRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -410,7 +411,9 @@ function RuntimeBlock({
{runtimeSource || agent.model ? (
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted-foreground">
{runtimeSource ? <span>{runtimeSource}</span> : null}
{agent.model ? <span>{agent.model}</span> : null}
{agent.model ? (
<span>{formatModelDisplayName(agent.model)}</span>
) : null}
</div>
) : null}
</div>
Expand Down
11 changes: 7 additions & 4 deletions desktop/src/features/agents/ui/ModelPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -221,7 +222,9 @@ export function ModelPicker({
<div className="px-3 py-2 text-sm text-muted-foreground">
{agent.model ? (
<>
<p className="font-medium text-foreground">{agent.model}</p>
<p className="font-medium text-foreground">
{formatModelDisplayName(agent.model)}
</p>
<p className="mt-0.5 text-xs">
This runtime does not support switching models.
</p>
Expand All @@ -237,7 +240,7 @@ export function ModelPicker({
>
{modelsData.models.map((model) => (
<DropdownMenuRadioItem key={model.id} value={model.id}>
{model.name ?? model.id}
{formatModelDisplayName(model.name ?? model.id)}
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
Expand Down
3 changes: 2 additions & 1 deletion desktop/src/features/agents/ui/agentConfigOptions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
5 changes: 3 additions & 2 deletions desktop/src/features/agents/ui/usePersonaModelDiscovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
formatModelDiscoveryErrorStatus,
type PersonaModelDiscoveryStatus,
} from "./personaModelDiscoveryStatus";
import { formatModelDisplayName } from "../lib/formatAgentModelLabel";
import type { PersonaModelOption } from "./agentConfigOptions";
import { providerRequiresExplicitModel } from "./agentConfigOptions";

Expand Down Expand Up @@ -64,7 +65,7 @@ export function getDiscoveredPersonaModelOptions(
provider === "relay-mesh"
? "Default (auto)"
: agentDefaultModel
? `Default model (${agentDefaultModel})`
? `Default model (${formatModelDisplayName(agentDefaultModel)})`
Comment thread
klopez4212 marked this conversation as resolved.
: "Default model",
},
];
Expand All @@ -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),
})),
];
}
Expand Down
5 changes: 4 additions & 1 deletion desktop/src/features/profile/ui/UserProfilePopover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -611,7 +612,9 @@ export function UserProfilePopover({
<InfoBadge>{runtimeLabel(relayAgent.agentType)}</InfoBadge>
) : null}
{managedAgent?.model ? (
<InfoBadge>{managedAgent.model}</InfoBadge>
<InfoBadge>
{formatModelDisplayName(managedAgent.model)}
</InfoBadge>
) : null}
{managedAgent?.acpCommand ? (
<InfoBadge>ACP: {managedAgent.acpCommand}</InfoBadge>
Expand Down
Loading