mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Expose model selection for provider-locked runtimes (#2071)
Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Pinky <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
@@ -3,11 +3,28 @@ import type { AgentAiConfigurationMode } from "./agentAiConfigurationPolicy";
|
||||
|
||||
export type { AgentAiConfigurationMode } from "./agentAiConfigurationPolicy";
|
||||
|
||||
export function HarnessModelDefaultNotice({
|
||||
model,
|
||||
}: {
|
||||
model?: string | null;
|
||||
}) {
|
||||
return (
|
||||
<div className="text-sm" data-testid="agent-harness-defaults-notice">
|
||||
<span className="text-muted-foreground">Model</span>{" "}
|
||||
<span className="text-foreground">
|
||||
{model?.trim() || "Harness default"}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function AgentAiConfigurationModeField({
|
||||
mode,
|
||||
needsProviderSelection = true,
|
||||
onModeChange,
|
||||
}: {
|
||||
mode: AgentAiConfigurationMode;
|
||||
needsProviderSelection?: boolean;
|
||||
onModeChange: (mode: AgentAiConfigurationMode) => void;
|
||||
}) {
|
||||
return (
|
||||
@@ -20,13 +37,19 @@ export function AgentAiConfigurationModeField({
|
||||
value={mode}
|
||||
>
|
||||
<TabsList>
|
||||
<TabsTrigger value="defaults">Use agent defaults</TabsTrigger>
|
||||
<TabsTrigger value="defaults">
|
||||
{needsProviderSelection
|
||||
? "Use agent defaults"
|
||||
: "Use harness defaults"}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="custom">Customize for this agent</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
{mode === "custom" ? (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Provider and model changes apply only to this agent.
|
||||
{needsProviderSelection
|
||||
? "Provider and model changes apply only to this agent."
|
||||
: "Model changes apply only to this agent."}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -73,6 +73,7 @@ import { AgentAiDefaultsNotice } from "./AgentAiDefaults";
|
||||
import { AgentAiDefaultsDialog } from "./AgentAiDefaultsDialog";
|
||||
import {
|
||||
AgentAiConfigurationModeField,
|
||||
HarnessModelDefaultNotice,
|
||||
type AgentAiConfigurationMode,
|
||||
} from "./AgentAiConfigurationMode";
|
||||
import {
|
||||
@@ -346,11 +347,14 @@ export function AgentDefinitionDialog({
|
||||
setIsCustomModelEditing(false);
|
||||
const nextPair = agentAiConfigurationPairForMode({
|
||||
current: { provider, model },
|
||||
inherited: {
|
||||
provider: inheritedProviderDefault.value,
|
||||
model: inheritedModelDefault.value,
|
||||
},
|
||||
inherited: runtimeCanChooseLlmProvider
|
||||
? {
|
||||
provider: inheritedProviderDefault.value,
|
||||
model: inheritedModelDefault.value,
|
||||
}
|
||||
: { provider: "", model: runtimeFileConfig?.model?.trim() ?? "" },
|
||||
mode: nextMode,
|
||||
needsProviderSelection: runtimeCanChooseLlmProvider,
|
||||
});
|
||||
setProvider(nextPair.provider);
|
||||
setModel(nextPair.model);
|
||||
@@ -428,8 +432,6 @@ export function AgentDefinitionDialog({
|
||||
aiConfigurationMode === "custom" && runtimeCanChooseLlmProvider;
|
||||
const modelFieldVisible =
|
||||
runtime.trim().length > 0 || blankRuntimeModelProviderEditable;
|
||||
// Customize pins a complete provider/model pair. Shared compute's concrete
|
||||
// automatic-routing value is the only valid non-model-id choice.
|
||||
const isExplicitModelRequired = aiConfigurationMode === "custom";
|
||||
// Gate the provider requirement on the field's actual visibility, not the raw
|
||||
// runtime capability. Codex/Claude hide the provider picker (they drive their
|
||||
@@ -852,9 +854,10 @@ export function AgentDefinitionDialog({
|
||||
{runtimeWarning}
|
||||
</div>
|
||||
|
||||
{llmProviderFieldVisible ? (
|
||||
{modelFieldVisible ? (
|
||||
<AgentAiConfigurationModeField
|
||||
mode={aiConfigurationMode}
|
||||
needsProviderSelection={runtimeCanChooseLlmProvider}
|
||||
onModeChange={handleAiConfigurationModeChange}
|
||||
/>
|
||||
) : null}
|
||||
@@ -950,19 +953,23 @@ export function AgentDefinitionDialog({
|
||||
</AnimatePresence>
|
||||
|
||||
{aiConfigurationMode === "defaults" ? (
|
||||
<AgentAiDefaultsNotice
|
||||
onEditDefaults={() => setAiDefaultsOpen(true)}
|
||||
triggerRef={aiDefaultsTriggerRef}
|
||||
explicitModel=""
|
||||
explicitProvider=""
|
||||
inheritedModel={inheritedModelDefault}
|
||||
inheritedProvider={inheritedProviderDefault}
|
||||
/>
|
||||
runtimeCanChooseLlmProvider ? (
|
||||
<AgentAiDefaultsNotice
|
||||
onEditDefaults={() => setAiDefaultsOpen(true)}
|
||||
triggerRef={aiDefaultsTriggerRef}
|
||||
explicitModel=""
|
||||
explicitProvider=""
|
||||
inheritedModel={inheritedModelDefault}
|
||||
inheritedProvider={inheritedProviderDefault}
|
||||
/>
|
||||
) : (
|
||||
<HarnessModelDefaultNotice model={runtimeFileConfig?.model} />
|
||||
)
|
||||
) : null}
|
||||
|
||||
<AgentAiDefaultsDialog
|
||||
onOpenChange={setAiDefaultsOpen}
|
||||
open={aiDefaultsOpen}
|
||||
open={runtimeCanChooseLlmProvider && aiDefaultsOpen}
|
||||
returnFocusRef={aiDefaultsTriggerRef}
|
||||
/>
|
||||
|
||||
|
||||
@@ -126,6 +126,20 @@ test("Defaults clears provider and model together", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("entering Customize pins only the harness model without a provider picker", () => {
|
||||
for (const model of ["claude-opus", "gpt-5.2-codex"]) {
|
||||
assert.deepEqual(
|
||||
agentAiConfigurationPairForMode({
|
||||
current: { provider: "", model: "" },
|
||||
inherited: { provider: "databricks_v2", model },
|
||||
mode: "custom",
|
||||
needsProviderSelection: false,
|
||||
}),
|
||||
{ provider: "", model },
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("entering Customize pins unresolved fields from the inherited pair", () => {
|
||||
assert.deepEqual(
|
||||
agentAiConfigurationPairForMode({
|
||||
|
||||
@@ -15,17 +15,21 @@ export function agentAiConfigurationPairForMode({
|
||||
current,
|
||||
inherited,
|
||||
mode,
|
||||
needsProviderSelection = true,
|
||||
}: {
|
||||
current: AgentAiConfigurationPair;
|
||||
inherited: AgentAiConfigurationPair;
|
||||
mode: AgentAiConfigurationMode;
|
||||
needsProviderSelection?: boolean;
|
||||
}): AgentAiConfigurationPair {
|
||||
if (mode === "defaults") {
|
||||
return { provider: "", model: "" };
|
||||
}
|
||||
|
||||
return {
|
||||
provider: current.provider.trim() || inherited.provider,
|
||||
provider: needsProviderSelection
|
||||
? current.provider.trim() || inherited.provider
|
||||
: "",
|
||||
model: current.model.trim() || inherited.model,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -129,6 +129,8 @@ test("Customize on Codex/Claude asks only for a model, never a provider", () =>
|
||||
);
|
||||
assert.match(reason, /Select a model/);
|
||||
assert.doesNotMatch(reason, /provider/);
|
||||
assert.match(reason, /Use harness defaults/);
|
||||
assert.doesNotMatch(reason, /Use AI defaults/);
|
||||
});
|
||||
|
||||
test("precedence: a missing name outranks incomplete AI defaults", () => {
|
||||
|
||||
@@ -128,7 +128,10 @@ export function personaSubmitBlock(
|
||||
if (input.customModelEmpty) pieces.push("a model");
|
||||
const what =
|
||||
pieces.length > 0 ? joinWithAnd(pieces) : "the AI configuration";
|
||||
return `Select ${what} for this agent, or switch to Use AI defaults.`;
|
||||
const defaultsLabel = input.runtimeNeedsProviderSelection
|
||||
? "Use AI defaults"
|
||||
: "Use harness defaults";
|
||||
return `Select ${what} for this agent, or switch to ${defaultsLabel}.`;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -197,4 +197,46 @@ test.describe("agent provider dropdown screenshots", () => {
|
||||
path: `${SHOTS}/03-builtin-edit-runtime-seeded.png`,
|
||||
});
|
||||
});
|
||||
|
||||
test("04-codex-definition-exposes-model-without-global-provider-defaults", async ({
|
||||
page,
|
||||
}) => {
|
||||
await installMockBridge(page, {
|
||||
globalAgentConfig: {
|
||||
provider: "databricks_v2",
|
||||
model: "global-databricks-model",
|
||||
env_vars: {},
|
||||
},
|
||||
personas: [
|
||||
{
|
||||
displayName: "Codex Definition",
|
||||
systemPrompt: "A Codex-backed definition.",
|
||||
runtime: "codex",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await page.goto("/");
|
||||
await page.getByTestId("open-agents-view").click();
|
||||
await page
|
||||
.getByRole("button", { name: "Open actions for Codex Definition" })
|
||||
.click();
|
||||
await page.getByRole("menuitem", { name: "Edit" }).click();
|
||||
|
||||
const dialog = page.getByTestId("persona-dialog");
|
||||
await expect(dialog).toBeVisible({ timeout: 10_000 });
|
||||
await expect(
|
||||
dialog.getByRole("tab", { name: "Customize for this agent" }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
dialog.getByText("Harness default", { exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(dialog.getByText(/Databricks/i)).toHaveCount(0);
|
||||
|
||||
await dialog.getByRole("tab", { name: "Customize for this agent" }).click();
|
||||
await expect(
|
||||
dialog.getByRole("combobox", { name: /model/i }),
|
||||
).toBeVisible();
|
||||
await expect(dialog.getByText("Model changes apply only")).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user