feat(desktop): Phase 2b pass 2 — production TS cutover and provider-aware labels

getProviderEffortConfig() is now generated-backed (thin wrapper over
resolveModelCapabilities() from modelCapabilities.ts). The old hand-table
implementation is preserved as getProviderEffortConfig_oldHandTable() for
the differential harness shim only — nothing user-facing imports the _old
name. AgentConfigFields.tsx and buzzAgentModelTuningFields.tsx continue
calling getProviderEffortConfig() with zero call-site churn and now receive
generated capabilities (including the F1 correction: databricks-gpt-5-5
allows only low|medium|high).

resolveModelLabel() gains an optional provider param that routes through
provider-qualified resolveModelCapabilities() registryLabel before falling
back to the unscoped DATABRICKS_MODEL_NAMES map. Provider threaded at all
provider-bearing call sites: ManagedAgentRow, ModelPicker, TeamIdentityCard,
usePersonaModelDiscovery, agentCardModelLabel, UnifiedAgentsSection, and
UserProfilePopover. AgentConfigFields fallbackModel label (genuinely
providerless) is left unscoped per plan.

Explicit .ts extension on modelCapabilities import in buzzAgentConfig.ts
(allowImportingTsExtensions is on) — enables Duncan to delete the
ts-esm-loader.mjs shebang bootstrap and run the differential script as
plain node --experimental-strip-types.

Test update: databricksModelNames.test.mjs regex updated to match the
provider-threaded call site resolveModelLabel(model.id, model.name, agent.provider).

All four gates at this head: typecheck clean, 3847/3847 unit pass, biome
clean (2 pre-existing infos), check:file-sizes and check:pubkey-truncation
pass.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
This commit is contained in:
npub1g8493u0xfsjrvflg4n08ezd7vec99mnwzlv0qgwpr9d7gvjwhuzqx59rhw
2026-07-31 13:47:02 -04:00
committed by npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7
parent c2bed46ee4
commit 7b019da1a4
10 changed files with 86 additions and 59 deletions

View File

@@ -22,8 +22,10 @@ import type { ManagedAgent } from "@/shared/api/types";
* than falling through to "inherited" for lack of an instance.
*/
export function resolveAgentCardModelLabel(input: {
agent: Pick<ManagedAgent, "modelSource" | "model"> | undefined;
agent: Pick<ManagedAgent, "modelSource" | "model" | "provider"> | undefined;
personaModel: string | null | undefined;
/** Inference provider for the persona/agent — threads provider-qualified label lookup. */
provider?: string | null | undefined;
defaultModel: string;
}): string {
if (input.agent) {
@@ -33,11 +35,11 @@ export function resolveAgentCardModelLabel(input: {
return formatDefaultModelLabel(input.defaultModel);
}
return input.agent.model?.trim()
? formatAgentModelLabel(input.agent.model)
? formatAgentModelLabel(input.agent.model, input.agent.provider)
: formatDefaultModelLabel(input.defaultModel);
}
return input.personaModel?.trim()
? formatAgentModelLabel(input.personaModel)
? formatAgentModelLabel(input.personaModel, input.provider)
: formatDefaultModelLabel(input.defaultModel);
}

View File

@@ -198,7 +198,7 @@ test("ModelPicker — discovered rows render through resolveModelLabel", () => {
assert.match(
source,
/<DropdownMenuRadioItem[\s\S]*?\{resolveModelLabel\(model\.id, model\.name\)\}/,
"dropdown rows must resolve labels through resolveModelLabel, not raw model.name/model.id",
/<DropdownMenuRadioItem[\s\S]*?\{resolveModelLabel\(model\.id, model\.name, agent\.provider\)\}/,
"dropdown rows must resolve labels through resolveModelLabel with provider, not raw model.name/model.id",
);
});

View File

@@ -1,11 +1,15 @@
import { DATABRICKS_MODEL_NAMES } from "../ui/modelCapabilities";
import {
DATABRICKS_MODEL_NAMES,
resolveModelCapabilities,
} from "../ui/modelCapabilities.ts";
/**
* Resolves a human-readable label for a model, following the three-tier
* precedence documented in AGENTS.md:
*
* 1. Nonblank discovered/API name (e.g. from AgentModelInfo.name)
* 2. Registry lookup by ID (generated registry_label table from model-capabilities manifest)
* 2. Registry lookup by ID — provider-qualified exact record first (when
* `provider` is supplied), then unscoped DATABRICKS_MODEL_NAMES map
* 3. Raw ID unchanged
*
* Returns the empty string when both id and discoveredName are blank.
@@ -14,11 +18,24 @@ import { DATABRICKS_MODEL_NAMES } from "../ui/modelCapabilities";
export function resolveModelLabel(
id: string,
discoveredName?: string | null | undefined,
provider?: string | null | undefined,
): string {
const trimmedName = discoveredName?.trim();
if (trimmedName) return trimmedName;
const trimmedId = id.trim();
if (!trimmedId) return "";
// Provider-qualified exact record (registry_label tier, provider-scoped).
if (provider) {
const trimmedProvider = provider.trim();
if (trimmedProvider) {
const registryLabel = resolveModelCapabilities(
trimmedProvider,
trimmedId,
).registryLabel;
if (registryLabel) return registryLabel;
}
}
// Unscoped registry map (handles legacy/provider-unknown persisted IDs).
return DATABRICKS_MODEL_NAMES.get(trimmedId) ?? trimmedId;
}
@@ -29,9 +46,15 @@ export function resolveModelLabel(
* For known Databricks managed endpoints the registry-curated name is returned
* (e.g. "databricks-gpt-5-5" → "GPT-5.5"). Unknown or custom endpoint IDs are
* returned unchanged — no heuristic string mangling.
*
* Pass `provider` when the inference provider is known to get a provider-qualified
* registry label (e.g. exact records in the generated manifest take priority).
*/
export function formatAgentModelLabel(model: string | null | undefined) {
export function formatAgentModelLabel(
model: string | null | undefined,
provider?: string | null | undefined,
) {
const trimmed = model?.trim();
if (!trimmed) return "Auto";
return resolveModelLabel(trimmed);
return resolveModelLabel(trimmed, null, provider);
}

View File

@@ -411,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>{resolveModelLabel(agent.model)}</span> : null}
{agent.model ? (
<span>{resolveModelLabel(agent.model, null, agent.provider)}</span>
) : null}
</div>
) : null}
</div>

View File

@@ -84,9 +84,9 @@ export function ModelPicker({
const currentValue = agent.model ?? modelsData?.agentDefaultModel ?? "";
const displayLabel = agent.model
? resolveModelLabel(agent.model)
? resolveModelLabel(agent.model, null, agent.provider)
: modelsData?.agentDefaultModel
? `${resolveModelLabel(modelsData.agentDefaultModel)} (default)`
? `${resolveModelLabel(modelsData.agentDefaultModel, null, agent.provider)} (default)`
: hasRequestedModels && loading
? "Loading..."
: "Auto";
@@ -223,7 +223,7 @@ export function ModelPicker({
{agent.model ? (
<>
<p className="font-medium text-foreground">
{resolveModelLabel(agent.model)}
{resolveModelLabel(agent.model, null, agent.provider)}
</p>
<p className="mt-0.5 text-xs">
This runtime does not support switching models.
@@ -240,7 +240,7 @@ export function ModelPicker({
>
{modelsData.models.map((model) => (
<DropdownMenuRadioItem key={model.id} value={model.id}>
{resolveModelLabel(model.id, model.name)}
{resolveModelLabel(model.id, model.name, agent.provider)}
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>

View File

@@ -203,7 +203,7 @@ function TeamAvatarItem({
function getTeamFooterModelLabel(personas: AgentPersona[]) {
const modelLabels = personas
.map((persona) => formatAgentModelLabel(persona.model))
.map((persona) => formatAgentModelLabel(persona.model, persona.provider))
.filter((model): model is string => Boolean(model));
if (modelLabels.length === 0) return "Auto";

View File

@@ -276,6 +276,7 @@ function AgentPersonaCard({
const modelLabel = resolveAgentCardModelLabel({
agent,
personaModel: persona.model,
provider: persona.provider,
defaultModel,
});
const isActive = agent ? isManagedAgentActive(agent) : false;

View File

@@ -1,11 +1,12 @@
/**
* Source-of-truth constants for buzz-agent model-tuning configuration knobs.
*
* Phase 2b: getProviderEffortConfigFromManifest() is the new generated-manifest
* path. getProviderEffortConfig() (legacy hand-tables) stays live for the
* differential harness until Phase 3 retires it.
* Phase 2b pass 2: getProviderEffortConfig() is now generated-backed (thin
* wrapper over resolveModelCapabilities()). The legacy hand-table implementation
* is preserved as getProviderEffortConfig_oldHandTable() for the differential
* harness only — nothing user-facing imports the _old shim. Phase 3 retires it.
*/
import { resolveModelCapabilities } from "./modelCapabilities";
import { resolveModelCapabilities } from "./modelCapabilities.ts";
/** Env var key for the thinking/effort level sent to the LLM. */
export const BUZZ_AGENT_THINKING_EFFORT = "BUZZ_AGENT_THINKING_EFFORT";
@@ -63,25 +64,40 @@ const ALL_VALUES = BUZZ_AGENT_THINKING_EFFORT_VALUES;
/**
* Returns the valid thinking-effort values and semantic default for the
* given provider and optional model string.
* given provider and optional model string, resolved from the generated
* model-capabilities manifest (modelCapabilities.ts).
*
* Model matching mirrors the Rust backend:
* - Anthropic: strip any endpoint-naming prefix, then test `is_manual_budget_model`
* / `is_adaptive_thinking_model` / `clamp_adaptive_effort` family checks.
* - OpenAI: strip any endpoint-naming prefix, then test `openai_efforts_for_model`
* family checks (boundary-aware: -pro before -5.x, digit/letter boundary).
* - DatabricksV2: strip prefix and route by model family.
* - Unknown/empty: all 7 values, default medium.
* This is the production entry point for all UI consumers. Resolution order:
* 1. Provider-qualified raw exact lookup
* 2. Provider-scoped family rules on normalized alias
* 3. Per-provider fallback
*
* Prefix stripping: finds the first occurrence of a known model-family token
* (`claude-`, `gpt-`) and drops everything before it. This handles any
* endpoint-naming convention (e.g. `databricks-`, `goose-`, `team-x-`) without
* maintaining an allowlist of known prefixes. If no family token is found, the
* raw model name is used as-is.
* The manifest's `supportedEfforts` maps to `validValues`; `defaultEffort`
* (which may be null for manual-budget models — "Inherit" is the natural
* default) maps to `defaultValue`.
*/
export function getProviderEffortConfig(
providerId: string,
model?: string,
): ProviderEffortConfig {
const cap = resolveModelCapabilities(providerId.toLowerCase(), model ?? "");
return {
validValues: cap.supportedEfforts,
defaultValue: cap.defaultEffort,
};
}
/**
* Legacy hand-table implementation — differential harness shim only.
*
* Preserved for the run-differential.mjs old-vs-new comparison until Phase 3
* retires it. Nothing user-facing should import this name.
*
* @deprecated Use getProviderEffortConfig() (generated-backed) instead.
*/
export function getProviderEffortConfig_oldHandTable(
providerId: string,
model?: string,
): ProviderEffortConfig {
const provider = providerId.toLowerCase();
// Strip arbitrary endpoint-naming prefix before model-family matching.
@@ -311,28 +327,5 @@ export function isBuzzAgentRuntime(runtimeId: string): boolean {
}
// ---------------------------------------------------------------------------
// Generated-manifest path (Phase 2b) — thin lookup over resolveModelCapabilities
// Differential harness support
// ---------------------------------------------------------------------------
/**
* Returns the valid thinking-effort values and semantic default for the
* given provider and model, resolved from the generated model-capabilities
* manifest (modelCapabilities.ts).
*
* This is the Phase 2b replacement path for getProviderEffortConfig().
* Both paths are live until Phase 3 retires getProviderEffortConfig().
*
* The manifest's `supportedEfforts` maps to `validValues`; `defaultEffort`
* (which may be null for manual-budget models — "Inherit" is the natural
* default) maps to `defaultValue`.
*/
export function getProviderEffortConfigFromManifest(
providerId: string,
model?: string,
): ProviderEffortConfig {
const cap = resolveModelCapabilities(providerId, model ?? "");
return {
validValues: cap.supportedEfforts,
defaultValue: cap.defaultEffort,
};
}

View File

@@ -65,7 +65,7 @@ export function getDiscoveredPersonaModelOptions(
provider === "relay-mesh"
? "Default (auto)"
: agentDefaultModel
? `Default model (${resolveModelLabel(agentDefaultModel)})`
? `Default model (${resolveModelLabel(agentDefaultModel, null, provider)})`
: "Default model",
},
];
@@ -78,7 +78,7 @@ export function getDiscoveredPersonaModelOptions(
...defaultModelOption,
...explicitModels.map((model) => ({
id: model.id,
label: resolveModelLabel(model.id, model.name),
label: resolveModelLabel(model.id, model.name, provider),
})),
];
}

View File

@@ -612,7 +612,13 @@ export function UserProfilePopover({
<InfoBadge>{runtimeLabel(relayAgent.agentType)}</InfoBadge>
) : null}
{managedAgent?.model ? (
<InfoBadge>{resolveModelLabel(managedAgent.model)}</InfoBadge>
<InfoBadge>
{resolveModelLabel(
managedAgent.model,
null,
managedAgent.provider,
)}
</InfoBadge>
) : null}
{managedAgent?.acpCommand ? (
<InfoBadge>ACP: {managedAgent.acpCommand}</InfoBadge>