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:
Wes
2026-07-18 09:13:42 -07:00
committed by GitHub
co-authored by Pinky
parent 1fdb73e17b
commit 3ad180a60f
7 changed files with 115 additions and 20 deletions
@@ -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();
});
});