feat: add admin-configurable chat title generation model (#24838)

Adds an admin-configurable deployment-wide setting that controls which
model is used for chat title generation. Admins can pick any enabled
chat model config from the Agents settings page, or leave the setting
unset to keep the existing fast-models-then-chat-model fallback
algorithm.

When a model is selected, both automatic and manual title generation use
only that model, with no silent fallback. When the configured model is
disabled, missing credentials, or otherwise unusable, automatic title
generation skips entirely (best-effort) and manual title regeneration
returns a clear error, so admins notice the misconfiguration instead of
silently routing title traffic through another provider.

## Surface

- New deployment-wide setting stored as a `site_configs` row
(`agents_chat_title_generation_model_override`).
- New experimental endpoint `GET/PUT
/api/experimental/chats/config/model-override/{context}`.
- Frontend: title generation now appears as a third dropdown on the
Agents admin settings page alongside the existing general and explore
context overrides.

## DRY refactors folded in

Title generation is integrated as a third value of the existing
`ChatModelOverrideContext` type alongside `general` and `explore`,
sharing the parameterized HTTP route, SDK methods, generated types, and
frontend API plumbing rather than introducing a parallel surface. The
`Agent` prefix was dropped from the type and route since title
generation is not a delegated agent.

The chatd model-override resolver is also shared.
`resolveConfiguredModelOverride` now takes a `failureMode` parameter:

- Subagent overrides use soft failure: misconfigured overrides are
logged and the parent model is used.
- Title generation uses hard failure: misconfigured overrides return an
explicit error so manual title regeneration surfaces the
misconfiguration and automatic title generation skips instead of
silently falling back.

> Mux is acting on Mike's behalf.
This commit is contained in:
Michael Suchacz
2026-05-04 13:13:00 +02:00
committed by GitHub
parent 203b0a9df8
commit 033ed0bb82
26 changed files with 1300 additions and 212 deletions
+10 -11
View File
@@ -3262,22 +3262,21 @@ class ExperimentalApiMethods {
);
};
getChatAgentModelOverride = async (
context: TypesGen.ChatAgentModelOverrideContext,
): Promise<TypesGen.ChatAgentModelOverrideResponse> => {
const response =
await this.axios.get<TypesGen.ChatAgentModelOverrideResponse>(
`/api/experimental/chats/config/agent-model-override/${encodeURIComponent(context)}`,
);
getChatModelOverride = async (
context: TypesGen.ChatModelOverrideContext,
): Promise<TypesGen.ChatModelOverrideResponse> => {
const response = await this.axios.get<TypesGen.ChatModelOverrideResponse>(
`/api/experimental/chats/config/model-override/${encodeURIComponent(context)}`,
);
return response.data;
};
updateChatAgentModelOverride = async (
context: TypesGen.ChatAgentModelOverrideContext,
req: TypesGen.UpdateChatAgentModelOverrideRequest,
updateChatModelOverride = async (
context: TypesGen.ChatModelOverrideContext,
req: TypesGen.UpdateChatModelOverrideRequest,
): Promise<void> => {
await this.axios.put(
`/api/experimental/chats/config/agent-model-override/${encodeURIComponent(context)}`,
`/api/experimental/chats/config/model-override/${encodeURIComponent(context)}`,
req,
);
};
+32 -28
View File
@@ -1320,25 +1320,6 @@ export interface Chat {
readonly children: readonly Chat[];
}
// From codersdk/chats.go
export type ChatAgentModelOverrideContext = "explore" | "general";
export const ChatAgentModelOverrideContexts: ChatAgentModelOverrideContext[] = [
"explore",
"general",
];
// From codersdk/chats.go
/**
* ChatAgentModelOverrideResponse is the response body for the chat agent
* model override configuration endpoint.
*/
export interface ChatAgentModelOverrideResponse {
readonly context: ChatAgentModelOverrideContext;
readonly model_config_id: string;
readonly is_malformed: boolean;
}
// From codersdk/chats.go
/**
* ChatAutoArchiveDaysResponse contains the current chat auto-archive setting.
@@ -2095,6 +2076,29 @@ export interface ChatModelOpenRouterProviderOptions {
readonly provider?: ChatModelOpenRouterProvider;
}
// From codersdk/chats.go
export type ChatModelOverrideContext =
| "explore"
| "general"
| "title_generation";
export const ChatModelOverrideContexts: ChatModelOverrideContext[] = [
"explore",
"general",
"title_generation",
];
// From codersdk/chats.go
/**
* ChatModelOverrideResponse is the response body for the chat model override
* configuration endpoint.
*/
export interface ChatModelOverrideResponse {
readonly context: ChatModelOverrideContext;
readonly model_config_id: string;
readonly is_malformed: boolean;
}
// From codersdk/chats.go
/**
* ChatModelProvider represents provider availability and model results.
@@ -7804,15 +7808,6 @@ export interface UpdateAppearanceConfig {
readonly announcement_banners: readonly BannerConfig[];
}
// From codersdk/chats.go
/**
* UpdateChatAgentModelOverrideRequest is the request body for updating the
* chat agent model override configuration endpoint.
*/
export interface UpdateChatAgentModelOverrideRequest {
readonly model_config_id: string;
}
// From codersdk/chats.go
/**
* UpdateChatAutoArchiveDaysRequest is a request to update the chat
@@ -7854,6 +7849,15 @@ export interface UpdateChatModelConfigRequest {
readonly model_config?: ChatModelCallConfig;
}
// From codersdk/chats.go
/**
* UpdateChatModelOverrideRequest is the request body for updating the chat
* model override configuration endpoint.
*/
export interface UpdateChatModelOverrideRequest {
readonly model_config_id: string;
}
// From codersdk/chats.go
/**
* UpdateChatPlanModeInstructionsRequest is the request body for
@@ -12,31 +12,30 @@ import { useAuthenticated } from "#/hooks/useAuthenticated";
import { RequirePermission } from "#/modules/permissions/RequirePermission";
import { AgentSettingsAgentsPageView } from "./AgentSettingsAgentsPageView";
const generalOverrideContext: TypesGen.ChatAgentModelOverrideContext =
"general";
const exploreOverrideContext: TypesGen.ChatAgentModelOverrideContext =
"explore";
const generalOverrideContext: TypesGen.ChatModelOverrideContext = "general";
const exploreOverrideContext: TypesGen.ChatModelOverrideContext = "explore";
const titleGenerationOverrideContext: TypesGen.ChatModelOverrideContext =
"title_generation";
const chatAgentModelOverrideKey = (
context: TypesGen.ChatAgentModelOverrideContext,
) => ["chat-agent-model-override", context] as const;
const chatModelOverrideKey = (context: TypesGen.ChatModelOverrideContext) =>
["chat-model-override", context] as const;
const chatAgentModelOverrideQuery = (
context: TypesGen.ChatAgentModelOverrideContext,
const chatModelOverrideQuery = (
context: TypesGen.ChatModelOverrideContext,
) => ({
queryKey: chatAgentModelOverrideKey(context),
queryFn: () => API.experimental.getChatAgentModelOverride(context),
queryKey: chatModelOverrideKey(context),
queryFn: () => API.experimental.getChatModelOverride(context),
});
const updateChatAgentModelOverrideMutation = (
const updateChatModelOverrideMutation = (
queryClient: QueryClient,
context: TypesGen.ChatAgentModelOverrideContext,
context: TypesGen.ChatModelOverrideContext,
) => ({
mutationFn: (req: TypesGen.UpdateChatAgentModelOverrideRequest) =>
API.experimental.updateChatAgentModelOverride(context, req),
mutationFn: (req: TypesGen.UpdateChatModelOverrideRequest) =>
API.experimental.updateChatModelOverride(context, req),
onSuccess: async () => {
await queryClient.invalidateQueries({
queryKey: chatAgentModelOverrideKey(context),
queryKey: chatModelOverrideKey(context),
exact: true,
});
},
@@ -48,25 +47,36 @@ const AgentSettingsAgentsPage: FC = () => {
const canEditDeploymentConfig = permissions.editDeploymentConfig;
const generalModelOverrideQuery = useQuery({
...chatAgentModelOverrideQuery(generalOverrideContext),
...chatModelOverrideQuery(generalOverrideContext),
enabled: canEditDeploymentConfig,
});
const exploreModelOverrideQuery = useQuery({
...chatAgentModelOverrideQuery(exploreOverrideContext),
...chatModelOverrideQuery(exploreOverrideContext),
enabled: canEditDeploymentConfig,
});
const titleGenerationModelQuery = useQuery({
...chatModelOverrideQuery(titleGenerationOverrideContext),
enabled: canEditDeploymentConfig,
});
const modelConfigsQuery = useQuery(chatModelConfigs());
const saveGeneralModelOverrideMutation = useMutation(
updateChatAgentModelOverrideMutation(queryClient, generalOverrideContext),
updateChatModelOverrideMutation(queryClient, generalOverrideContext),
);
const saveTitleGenerationModelMutation = useMutation(
updateChatModelOverrideMutation(
queryClient,
titleGenerationOverrideContext,
),
);
const saveExploreModelOverrideMutation = useMutation(
updateChatAgentModelOverrideMutation(queryClient, exploreOverrideContext),
updateChatModelOverrideMutation(queryClient, exploreOverrideContext),
);
return (
<RequirePermission isFeatureVisible={canEditDeploymentConfig}>
<AgentSettingsAgentsPageView
generalModelOverrideData={generalModelOverrideQuery.data}
titleGenerationModelOverrideData={titleGenerationModelQuery.data}
exploreModelOverrideData={exploreModelOverrideQuery.data}
modelConfigsData={modelConfigsQuery.data}
modelConfigsError={modelConfigsQuery.error}
@@ -78,6 +88,13 @@ const AgentSettingsAgentsPage: FC = () => {
isSaveGeneralModelOverrideError={
saveGeneralModelOverrideMutation.isError
}
onSaveTitleGenerationModel={saveTitleGenerationModelMutation.mutate}
isSavingTitleGenerationModel={
saveTitleGenerationModelMutation.isPending
}
isSaveTitleGenerationModelError={
saveTitleGenerationModelMutation.isError
}
onSaveExploreModelOverride={saveExploreModelOverrideMutation.mutate}
isSavingExploreModelOverride={
saveExploreModelOverrideMutation.isPending
@@ -10,6 +10,8 @@ const OVERRIDE_MALFORMED_WARNING =
"The saved override is malformed and is being treated as unset. Click Save to clear it.";
const UNAVAILABLE_SAVED_MODEL_WARNING =
"The saved model is no longer enabled and will be ignored until you choose a new override.";
const TITLE_UNAVAILABLE_SAVED_MODEL_WARNING =
"The selected model is currently unavailable. Title generation will be skipped until you choose another model or clear this setting.";
const buildModelConfig = (
overrides: Partial<TypesGen.ChatModelConfig>,
@@ -28,15 +30,20 @@ const buildModelConfig = (
});
const buildOverrideData = (
context: TypesGen.ChatAgentModelOverrideContext,
overrides: Partial<TypesGen.ChatAgentModelOverrideResponse> = {},
): TypesGen.ChatAgentModelOverrideResponse => ({
context: TypesGen.ChatModelOverrideContext,
overrides: Partial<TypesGen.ChatModelOverrideResponse> = {},
): TypesGen.ChatModelOverrideResponse => ({
context,
model_config_id: "",
is_malformed: false,
...overrides,
});
const buildTitleGenerationModelOverrideData = (
overrides: Partial<TypesGen.ChatModelOverrideResponse> = {},
): TypesGen.ChatModelOverrideResponse =>
buildOverrideData("title_generation", overrides);
const generalModelConfig = buildModelConfig({
id: "model-general-gpt-4.1-mini",
display_name: "GPT 4.1 Mini",
@@ -50,6 +57,13 @@ const claudeSonnetModelConfig = buildModelConfig({
context_limit: 200_000,
});
const titleModelConfig = buildModelConfig({
id: "model-title-gpt-4o-mini",
model: "gpt-4o-mini",
display_name: "GPT 4o Mini",
context_limit: 128_000,
});
const exploreFallbackModelConfig = buildModelConfig({
id: "model-explore-blank-display",
provider: "anthropic",
@@ -65,6 +79,14 @@ const generalDisabledModelConfig = buildModelConfig({
enabled: false,
});
const titleDisabledModelConfig = buildModelConfig({
id: "model-title-disabled",
model: "gpt-4o-mini-legacy",
display_name: "GPT 4o Mini Legacy",
enabled: false,
context_limit: 128_000,
});
const exploreDisabledModelConfig = buildModelConfig({
id: "model-explore-disabled",
provider: "anthropic",
@@ -77,8 +99,10 @@ const exploreDisabledModelConfig = buildModelConfig({
const allModelConfigs: TypesGen.ChatModelConfig[] = [
generalModelConfig,
claudeSonnetModelConfig,
titleModelConfig,
exploreFallbackModelConfig,
generalDisabledModelConfig,
titleDisabledModelConfig,
exploreDisabledModelConfig,
];
@@ -86,6 +110,7 @@ const makeArgs = (
overrides: Partial<AgentSettingsAgentsPageViewProps> = {},
): AgentSettingsAgentsPageViewProps => ({
generalModelOverrideData: buildOverrideData("general"),
titleGenerationModelOverrideData: buildTitleGenerationModelOverrideData(),
exploreModelOverrideData: buildOverrideData("explore"),
modelConfigsData: allModelConfigs,
modelConfigsError: undefined,
@@ -93,6 +118,9 @@ const makeArgs = (
onSaveGeneralModelOverride: fn(),
isSavingGeneralModelOverride: false,
isSaveGeneralModelOverrideError: false,
onSaveTitleGenerationModel: fn(),
isSavingTitleGenerationModel: false,
isSaveTitleGenerationModelError: false,
onSaveExploreModelOverride: fn(),
isSavingExploreModelOverride: false,
isSaveExploreModelOverrideError: false,
@@ -146,13 +174,28 @@ export const AllOverridesUnset: Story = {
const headings = await canvas.findAllByRole("heading", { level: 3 });
expect(headings.map((heading) => heading.textContent?.trim())).toEqual([
"General model",
"Title generation model",
"Explore subagent model",
]);
await canvas.findByText(
"Choose a model for generated chat titles. Leave unset to use Coder's default title algorithm, which currently tries fast title models for configured providers first, for example Claude Haiku, GPT-4o mini, and Gemini Flash, then falls back to the chat's current model. When a model is selected here, Coder uses only that model for title generation. Recommended title models are fast and low cost.",
);
for (const headingName of ["General model", "Explore subagent model"]) {
const unsetSections = [
{ headingName: "General model", placeholder: "Use chat default" },
{
headingName: "Title generation model",
placeholder: "Use title default",
},
{
headingName: "Explore subagent model",
placeholder: "Use chat default",
},
];
for (const { headingName, placeholder } of unsetSections) {
const section = await getSection(canvasElement, headingName);
expect(
within(section).getByRole("combobox", { name: "Use chat default" }),
within(section).getByRole("combobox", { name: placeholder }),
).toBeInTheDocument();
expect(
within(section).getByRole("button", { name: "Save" }),
@@ -166,12 +209,19 @@ export const EachOverrideSetToEnabledModel: Story = {
generalModelOverrideData: buildOverrideData("general", {
model_config_id: generalModelConfig.id,
}),
titleGenerationModelOverrideData: buildTitleGenerationModelOverrideData({
model_config_id: titleModelConfig.id,
}),
exploreModelOverrideData: buildOverrideData("explore", {
model_config_id: exploreFallbackModelConfig.id,
}),
}),
play: async ({ canvasElement, args }) => {
const generalSection = await getSection(canvasElement, "General model");
const titleSection = await getSection(
canvasElement,
"Title generation model",
);
const exploreSection = await getSection(
canvasElement,
"Explore subagent model",
@@ -183,6 +233,12 @@ export const EachOverrideSetToEnabledModel: Story = {
}),
).toHaveTextContent("claude-sonnet-4-20250514");
expect(
within(titleSection).getByRole("combobox", {
name: /gpt 4o mini/i,
}),
).toHaveTextContent("GPT 4o Mini");
await selectModelInSection(
generalSection,
canvasElement,
@@ -203,6 +259,26 @@ export const EachOverrideSetToEnabledModel: Story = {
);
});
await selectModelInSection(
titleSection,
canvasElement,
/gpt 4o mini/i,
"Claude Sonnet 4",
);
const titleSaveButton = within(titleSection).getByRole("button", {
name: "Save",
});
await waitFor(() => {
expect(titleSaveButton).toBeEnabled();
});
await userEvent.click(titleSaveButton);
await waitFor(() => {
expect(args.onSaveTitleGenerationModel).toHaveBeenCalledWith(
{ model_config_id: claudeSonnetModelConfig.id },
expect.anything(),
);
});
const exploreClearButton = within(exploreSection).getByRole("button", {
name: "Clear",
});
@@ -228,18 +304,25 @@ export const MalformedOverridesRemainClearableAndSaveable: Story = {
generalModelOverrideData: buildOverrideData("general", {
is_malformed: true,
}),
titleGenerationModelOverrideData: buildTitleGenerationModelOverrideData({
is_malformed: true,
}),
exploreModelOverrideData: buildOverrideData("explore", {
is_malformed: true,
}),
}),
play: async ({ canvasElement, args }) => {
const generalSection = await getSection(canvasElement, "General model");
const titleSection = await getSection(
canvasElement,
"Title generation model",
);
const exploreSection = await getSection(
canvasElement,
"Explore subagent model",
);
for (const section of [generalSection, exploreSection]) {
for (const section of [generalSection, titleSection, exploreSection]) {
await within(section).findByText(OVERRIDE_MALFORMED_WARNING);
}
@@ -257,6 +340,20 @@ export const MalformedOverridesRemainClearableAndSaveable: Story = {
);
});
const titleSaveButton = within(titleSection).getByRole("button", {
name: "Save",
});
await waitFor(() => {
expect(titleSaveButton).toBeEnabled();
});
await userEvent.click(titleSaveButton);
await waitFor(() => {
expect(args.onSaveTitleGenerationModel).toHaveBeenCalledWith(
{ model_config_id: "" },
expect.anything(),
);
});
const exploreSaveButton = within(exploreSection).getByRole("button", {
name: "Save",
});
@@ -278,12 +375,19 @@ export const UnavailableSavedModels: Story = {
generalModelOverrideData: buildOverrideData("general", {
model_config_id: generalDisabledModelConfig.id,
}),
titleGenerationModelOverrideData: buildTitleGenerationModelOverrideData({
model_config_id: titleDisabledModelConfig.id,
}),
exploreModelOverrideData: buildOverrideData("explore", {
model_config_id: exploreDisabledModelConfig.id,
}),
}),
play: async ({ canvasElement }) => {
const generalSection = await getSection(canvasElement, "General model");
const titleSection = await getSection(
canvasElement,
"Title generation model",
);
const exploreSection = await getSection(
canvasElement,
"Explore subagent model",
@@ -295,5 +399,13 @@ export const UnavailableSavedModels: Story = {
within(section).getByRole("combobox", { name: "Unavailable model" }),
).toBeInTheDocument();
}
await within(titleSection).findByText(
TITLE_UNAVAILABLE_SAVED_MODEL_WARNING,
);
expect(
within(titleSection).getByRole("combobox", {
name: "Unavailable model",
}),
).toBeInTheDocument();
},
};
@@ -7,19 +7,23 @@ import {
} from "./components/SubagentModelOverrideSettings";
type SaveModelOverride = (
req: TypesGen.UpdateChatAgentModelOverrideRequest,
req: { readonly model_config_id: string },
options?: MutationCallbacks,
) => void;
export interface AgentSettingsAgentsPageViewProps {
generalModelOverrideData?: TypesGen.ChatAgentModelOverrideResponse;
exploreModelOverrideData?: TypesGen.ChatAgentModelOverrideResponse;
generalModelOverrideData?: TypesGen.ChatModelOverrideResponse;
titleGenerationModelOverrideData?: TypesGen.ChatModelOverrideResponse;
exploreModelOverrideData?: TypesGen.ChatModelOverrideResponse;
modelConfigsData: TypesGen.ChatModelConfig[] | undefined;
modelConfigsError: unknown;
isLoadingModelConfigs: boolean;
onSaveGeneralModelOverride?: SaveModelOverride;
isSavingGeneralModelOverride?: boolean;
isSaveGeneralModelOverrideError?: boolean;
onSaveTitleGenerationModel: SaveModelOverride;
isSavingTitleGenerationModel: boolean;
isSaveTitleGenerationModelError: boolean;
onSaveExploreModelOverride: SaveModelOverride;
isSavingExploreModelOverride: boolean;
isSaveExploreModelOverrideError: boolean;
@@ -29,6 +33,7 @@ export const AgentSettingsAgentsPageView: FC<
AgentSettingsAgentsPageViewProps
> = ({
generalModelOverrideData,
titleGenerationModelOverrideData,
exploreModelOverrideData,
modelConfigsData,
modelConfigsError,
@@ -36,6 +41,9 @@ export const AgentSettingsAgentsPageView: FC<
onSaveGeneralModelOverride,
isSavingGeneralModelOverride = false,
isSaveGeneralModelOverrideError = false,
onSaveTitleGenerationModel,
isSavingTitleGenerationModel,
isSaveTitleGenerationModelError,
onSaveExploreModelOverride,
isSavingExploreModelOverride,
isSaveExploreModelOverrideError,
@@ -77,6 +85,31 @@ export const AgentSettingsAgentsPageView: FC<
/>
</section>
)}
<section
aria-label="Title generation model"
className="flex flex-col gap-3"
>
<SectionHeader
label="Title generation model"
description="Choose a model for generated chat titles. Leave unset to use Coder's default title algorithm, which currently tries fast title models for configured providers first, for example Claude Haiku, GPT-4o mini, and Gemini Flash, then falls back to the chat's current model. When a model is selected here, Coder uses only that model for title generation. Recommended title models are fast and low cost."
level="section"
/>
<SubagentModelOverrideSettings
title="Title generation model"
description="Choose a model for generated chat titles."
modelOverrideData={titleGenerationModelOverrideData}
enabledModelConfigs={enabledModelConfigs}
modelConfigsError={modelConfigsError}
isLoading={isLoadingModelConfigs}
onSaveModelOverride={onSaveTitleGenerationModel}
isSaving={isSavingTitleGenerationModel}
isSaveError={isSaveTitleGenerationModelError}
saveErrorMessage="Failed to save title generation model."
unsetPlaceholder="Use title default"
unavailableModelWarning="The selected model is currently unavailable. Title generation will be skipped until you choose another model or clear this setting."
showHeader={false}
/>
</section>
<section
aria-label="Explore subagent model"
className="flex flex-col gap-3"
@@ -161,9 +161,17 @@ const AgentsRouteElement = () => (
model_config_id: "",
is_malformed: false,
}}
titleGenerationModelOverrideData={{
context: "title_generation",
model_config_id: "",
is_malformed: false,
}}
modelConfigsData={[]}
modelConfigsError={undefined}
isLoadingModelConfigs={false}
onSaveTitleGenerationModel={fn()}
isSavingTitleGenerationModel={false}
isSaveTitleGenerationModelError={false}
onSaveExploreModelOverride={fn()}
isSavingExploreModelOverride={false}
isSaveExploreModelOverrideError={false}
@@ -22,7 +22,7 @@ interface UpdateModelOverrideRequest {
interface SubagentModelOverrideSettingsProps {
title: string;
description: ReactNode;
description?: ReactNode;
modelOverrideData: ModelOverrideData | undefined;
enabledModelConfigs: readonly TypesGen.ChatModelConfig[];
modelConfigsError: unknown;
@@ -34,6 +34,8 @@ interface SubagentModelOverrideSettingsProps {
isSaving: boolean;
isSaveError: boolean;
saveErrorMessage: string;
unsetPlaceholder?: string;
unavailableModelWarning?: string;
showHeader?: boolean;
disabled?: boolean;
}
@@ -61,6 +63,8 @@ export const SubagentModelOverrideSettings: FC<
isSaving,
isSaveError,
saveErrorMessage,
unsetPlaceholder = "Use chat default",
unavailableModelWarning = "The saved model is no longer enabled and will be ignored until you choose a new override.",
showHeader = true,
disabled = false,
}) => {
@@ -104,9 +108,11 @@ export const SubagentModelOverrideSettings: FC<
<h3 className="m-0 text-[13px] font-semibold text-content-primary">
{title}
</h3>
<p className="!mt-0.5 m-0 text-xs text-content-secondary">
{description}
</p>
{description && (
<p className="!mt-0.5 m-0 text-xs text-content-secondary">
{description}
</p>
)}
</>
)}
<ModelSelector
@@ -115,7 +121,7 @@ export const SubagentModelOverrideSettings: FC<
onValueChange={(value) => form.setFieldValue("model_config_id", value)}
disabled={isModelOverrideDisabled}
placeholder={
isUnavailableSavedModel ? "Unavailable model" : "Use chat default"
isUnavailableSavedModel ? "Unavailable model" : unsetPlaceholder
}
emptyMessage={
isLoading ? "Loading models..." : "No enabled models found."
@@ -125,10 +131,7 @@ export const SubagentModelOverrideSettings: FC<
/>
{isUnavailableSavedModel && (
<Alert severity="warning">
<AlertDescription>
The saved model is no longer enabled and will be ignored until you
choose a new override.
</AlertDescription>
<AlertDescription>{unavailableModelWarning}</AlertDescription>
</Alert>
)}
{isMalformedOverride && (