From 7b019da1a4f1ee30b7b2b0933cd12ec9eabfa4eb Mon Sep 17 00:00:00 2001 From: npub1g8493u0xfsjrvflg4n08ezd7vec99mnwzlv0qgwpr9d7gvjwhuzqx59rhw <41ea58f1e64c243627e8acde7c89be667052ee6e17d8f021c1195be4324ebf04@buzz.block.builderlab.xyz> Date: Fri, 31 Jul 2026 13:47:02 -0400 Subject: [PATCH] =?UTF-8?q?feat(desktop):=20Phase=202b=20pass=202=20?= =?UTF-8?q?=E2=80=94=20production=20TS=20cutover=20and=20provider-aware=20?= =?UTF-8?q?labels?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Signed-off-by: Will Pfleger --- .../agents/lib/agentCardModelLabel.ts | 8 +- .../agents/lib/databricksModelNames.test.mjs | 4 +- .../agents/lib/formatAgentModelLabel.ts | 31 +++++++- .../features/agents/ui/ManagedAgentRow.tsx | 4 +- .../src/features/agents/ui/ModelPicker.tsx | 8 +- .../features/agents/ui/TeamIdentityCard.tsx | 2 +- .../agents/ui/UnifiedAgentsSection.tsx | 1 + .../src/features/agents/ui/buzzAgentConfig.ts | 75 +++++++++---------- .../agents/ui/usePersonaModelDiscovery.ts | 4 +- .../profile/ui/UserProfilePopover.tsx | 8 +- 10 files changed, 86 insertions(+), 59 deletions(-) diff --git a/desktop/src/features/agents/lib/agentCardModelLabel.ts b/desktop/src/features/agents/lib/agentCardModelLabel.ts index 81b52b021d..df330c46dd 100644 --- a/desktop/src/features/agents/lib/agentCardModelLabel.ts +++ b/desktop/src/features/agents/lib/agentCardModelLabel.ts @@ -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 | undefined; + agent: Pick | 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); } diff --git a/desktop/src/features/agents/lib/databricksModelNames.test.mjs b/desktop/src/features/agents/lib/databricksModelNames.test.mjs index 2f7cec41dc..37970e86b9 100644 --- a/desktop/src/features/agents/lib/databricksModelNames.test.mjs +++ b/desktop/src/features/agents/lib/databricksModelNames.test.mjs @@ -198,7 +198,7 @@ test("ModelPicker — discovered rows render through resolveModelLabel", () => { assert.match( source, - / {runtimeSource ? {runtimeSource} : null} - {agent.model ? {resolveModelLabel(agent.model)} : null} + {agent.model ? ( + {resolveModelLabel(agent.model, null, agent.provider)} + ) : null} ) : null} diff --git a/desktop/src/features/agents/ui/ModelPicker.tsx b/desktop/src/features/agents/ui/ModelPicker.tsx index 41fe0d2562..863cd85119 100644 --- a/desktop/src/features/agents/ui/ModelPicker.tsx +++ b/desktop/src/features/agents/ui/ModelPicker.tsx @@ -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 ? ( <>

- {resolveModelLabel(agent.model)} + {resolveModelLabel(agent.model, null, agent.provider)}

This runtime does not support switching models. @@ -240,7 +240,7 @@ export function ModelPicker({ > {modelsData.models.map((model) => ( - {resolveModelLabel(model.id, model.name)} + {resolveModelLabel(model.id, model.name, agent.provider)} ))} diff --git a/desktop/src/features/agents/ui/TeamIdentityCard.tsx b/desktop/src/features/agents/ui/TeamIdentityCard.tsx index 19596b76d6..8e4b02c9e8 100644 --- a/desktop/src/features/agents/ui/TeamIdentityCard.tsx +++ b/desktop/src/features/agents/ui/TeamIdentityCard.tsx @@ -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"; diff --git a/desktop/src/features/agents/ui/UnifiedAgentsSection.tsx b/desktop/src/features/agents/ui/UnifiedAgentsSection.tsx index 19a5ef1171..ffb8c256e0 100644 --- a/desktop/src/features/agents/ui/UnifiedAgentsSection.tsx +++ b/desktop/src/features/agents/ui/UnifiedAgentsSection.tsx @@ -276,6 +276,7 @@ function AgentPersonaCard({ const modelLabel = resolveAgentCardModelLabel({ agent, personaModel: persona.model, + provider: persona.provider, defaultModel, }); const isActive = agent ? isManagedAgentActive(agent) : false; diff --git a/desktop/src/features/agents/ui/buzzAgentConfig.ts b/desktop/src/features/agents/ui/buzzAgentConfig.ts index e9c931a980..79dd680e4e 100644 --- a/desktop/src/features/agents/ui/buzzAgentConfig.ts +++ b/desktop/src/features/agents/ui/buzzAgentConfig.ts @@ -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, - }; -} diff --git a/desktop/src/features/agents/ui/usePersonaModelDiscovery.ts b/desktop/src/features/agents/ui/usePersonaModelDiscovery.ts index 5966c576a6..8f2ef49346 100644 --- a/desktop/src/features/agents/ui/usePersonaModelDiscovery.ts +++ b/desktop/src/features/agents/ui/usePersonaModelDiscovery.ts @@ -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), })), ]; } diff --git a/desktop/src/features/profile/ui/UserProfilePopover.tsx b/desktop/src/features/profile/ui/UserProfilePopover.tsx index 2c76fdc796..b5f40ec157 100644 --- a/desktop/src/features/profile/ui/UserProfilePopover.tsx +++ b/desktop/src/features/profile/ui/UserProfilePopover.tsx @@ -612,7 +612,13 @@ export function UserProfilePopover({ {runtimeLabel(relayAgent.agentType)} ) : null} {managedAgent?.model ? ( - {resolveModelLabel(managedAgent.model)} + + {resolveModelLabel( + managedAgent.model, + null, + managedAgent.provider, + )} + ) : null} {managedAgent?.acpCommand ? ( ACP: {managedAgent.acpCommand}