mirror of
https://github.com/block/buzz.git
synced 2026-11-01 15:01:12 +00:00
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:
committed by
npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7
parent
c2bed46ee4
commit
7b019da1a4
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -276,6 +276,7 @@ function AgentPersonaCard({
|
||||
const modelLabel = resolveAgentCardModelLabel({
|
||||
agent,
|
||||
personaModel: persona.model,
|
||||
provider: persona.provider,
|
||||
defaultModel,
|
||||
});
|
||||
const isActive = agent ? isManagedAgentActive(agent) : false;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
})),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user