Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
a1b5605
refactor(web): extract the model setting controls into a shared module
nmgarza5 Aug 19, 2026
5d5970d
feat(web): model settings popover in the LLM provider modal
nmgarza5 Aug 19, 2026
bae3b0f
fix(web): keep unsaved model edits across the auto-update toggle
nmgarza5 Aug 20, 2026
b3b02b2
fix(web): match the model settings popover to the mock
nmgarza5 Aug 20, 2026
7c3d664
fix(web): route well-known providers with empty custom_config to thei…
nmgarza5 Aug 20, 2026
1cd0606
feat(web): model settings for custom provider models
nmgarza5 Aug 20, 2026
585a5e5
feat(web): set the org default model from a provider model row
nmgarza5 Aug 20, 2026
548b669
fix(opal): keep an editable title click out of parent click handlers
nmgarza5 Aug 20, 2026
a0f8792
fix(web): stop the settings popover from scrolling the provider modal
nmgarza5 Aug 20, 2026
acb7dd4
fix(web): match the default model row actions to the mock
nmgarza5 Aug 20, 2026
f398845
fix(web): keep refetched models across the auto-update toggle
nmgarza5 Aug 20, 2026
9051189
fix(web): rewrite a stale reasoning default when it exceeds the new max
nmgarza5 Aug 20, 2026
2258351
fix(web): clamp stored custom model settings to current capability
nmgarza5 Aug 20, 2026
71af9e5
fix(web): scroll the settings popover instead of clipping it
nmgarza5 Aug 20, 2026
958ef26
fix(web): persist custom model settings on provider save
nmgarza5 Aug 20, 2026
cefa34c
fix(web): fit the settings popover to the mock height
nmgarza5 Aug 20, 2026
170b9eb
refactor(web): review pass over the model settings feature
nmgarza5 Aug 20, 2026
742fd99
fix(web): keep a rename-committing click off the visibility toggle
nmgarza5 Aug 20, 2026
4e8baa2
fix(web): scope the rename guard to the title row's edit input
nmgarza5 Aug 21, 2026
db6222a
refactor(web): build the settings popover from Opal components
nmgarza5 Aug 21, 2026
55153b7
fix(web): uniform range for the default reasoning slider
nmgarza5 Aug 21, 2026
812852b
refactor(opal): editHandle presence hides the built-in pencil
nmgarza5 Aug 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions web/lib/opal/src/layouts/content/ContentMd.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import SvgXOctagon from "@opal/icons/x-octagon";
import type { IconFunctionComponent, RichStr } from "@opal/types";
import { toPlainString } from "@opal/components/text/InlineMarkdown";
import { cn } from "@opal/utils";
import { useEffect, useRef, useState } from "react";
import { useEffect, useRef, useState, useImperativeHandle } from "react";

// ---------------------------------------------------------------------------
// Types
Expand Down Expand Up @@ -39,6 +39,10 @@ interface ContentMdPresetConfig {
descriptionIndent: string;
}

export interface ContentMdEditHandle {
startEditing: () => void;
}

interface ContentMdProps {
/** Optional icon component. */
icon?: IconFunctionComponent;
Expand All @@ -62,6 +66,10 @@ interface ContentMdProps {
/** Enable inline editing of the title. */
editable?: boolean;

/** Handle for starting a title edit from an external control. Setting it
* hides the built-in pencil. */
editHandle?: React.Ref<ContentMdEditHandle>;

/** Called when the user commits an edit. */
onTitleChange?: (newTitle: string) => void;

Expand Down Expand Up @@ -152,6 +160,7 @@ function ContentMd({
titleMaxLines,
sizePreset = "main-ui",
ref,
editHandle,
}: ContentMdProps) {
const [editing, setEditing] = useState(false);
const [editValue, setEditValue] = useState(toPlainString(title));
Expand All @@ -168,6 +177,13 @@ function ContentMd({
setEditValue(toPlainString(title));
setEditing(true);
}
useImperativeHandle(editHandle, () => ({ startEditing }), [title]);

// Starting an edit must not double as a click on the parent row.
function handleTitleClick(e: React.MouseEvent) {
e.stopPropagation();
startEditing();
}

function commit() {
const value = editValue.trim();
Expand Down Expand Up @@ -244,7 +260,7 @@ function ContentMd({
color="inherit"
maxLines={titleMaxLines}
title={toPlainString(title)}
onClick={editable ? startEditing : undefined}
onClick={editable ? handleTitleClick : undefined}
>
{title}
</Text>
Expand Down Expand Up @@ -277,7 +293,7 @@ function ContentMd({

{tag && <Tag {...tag} />}

{editable && !editing && (
{editable && !editing && editHandle == null && (
<div
className={cn(
"opal-content-md-edit-button",
Expand Down
5 changes: 5 additions & 0 deletions web/lib/opal/src/layouts/content/components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
} from "@opal/layouts/content/ContentLg";
import {
ContentMd,
type ContentMdEditHandle,
type ContentMdProps,
} from "@opal/layouts/content/ContentMd";
import type { TagProps } from "@opal/components";
Expand Down Expand Up @@ -54,6 +55,10 @@ interface ContentBaseProps {
/** Enable inline editing of the title. */
editable?: boolean;

/** Handle for starting a title edit from an external control. Setting it
* hides the built-in pencil. */
editHandle?: React.Ref<ContentMdEditHandle>;

/** Called when the user commits an edit. */
onTitleChange?: (newTitle: string) => void;

Expand Down
17 changes: 17 additions & 0 deletions web/src/lib/languageModels/cache.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { ScopedMutator } from "swr";
import { toast } from "@opal/layouts";
import { SWR_KEYS } from "@/lib/swr-keys";
import { setDefaultLlmModel } from "@/lib/languageModels/svc";

const PERSONA_PROVIDER_ENDPOINT_PATTERN =
/^\/api\/llm\/persona\/\d+\/providers$/;
Expand All @@ -16,3 +18,18 @@ export async function refreshLlmProviderCaches(
),
]);
}

export async function setDefaultLlmModelAndRefresh(
providerId: number,
modelName: string,
mutate: ScopedMutator
): Promise<void> {
try {
await setDefaultLlmModel(providerId, modelName);
await refreshLlmProviderCaches(mutate);
toast.success("Default model updated successfully!");
} catch (e) {
const message = e instanceof Error ? e.message : "Unknown error";
toast.error(`Failed to set default model: ${message}`);
}
}
10 changes: 7 additions & 3 deletions web/src/lib/languageModels/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,8 @@ const DEFAULT_ENTRY: ProviderEntry = {
Modal: CustomModal,
};

// Providers that don't use custom_config themselves — if custom_config is
// present it means the provider was originally created via CustomModal.
// Providers that don't use custom_config themselves, so a non-empty
// custom_config means the provider was originally created via CustomModal.
const CUSTOM_CONFIG_OVERRIDES = new Set<string>([
LLMProviderName.OPENAI,
LLMProviderName.ANTHROPIC,
Expand All @@ -168,8 +168,12 @@ export function getProvider(
companyName: providerName,
};

// An empty custom_config carries no signal of origin. Only a non-empty map
// marks a provider created via the custom form.
const customConfig = existingProvider?.custom_config;
if (
existingProvider?.custom_config != null &&
customConfig != null &&
Object.keys(customConfig).length > 0 &&
CUSTOM_CONFIG_OVERRIDES.has(providerName)
) {
return { ...entry, Modal: CustomModal };
Expand Down
5 changes: 5 additions & 0 deletions web/src/lib/languageModels/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ export interface ModelConfiguration {
* case the picker falls back to the levels every reasoning model supports.
*/
supported_reasoning_efforts?: ReasoningEffortOverride[];
/** What the admin permits or defaults, distinct from
* supported_reasoning_efforts (what the model can do). Null means unset. */
reasoning_effort_max?: ReasoningEffortOverride | null;
reasoning_effort_default?: ReasoningEffortOverride | null;
temperature_default?: number | null;
/** Display-only metadata surfaced in the model picker (Nebius TokenFactory). */
quantization?: string | null;
country_code?: string | null;
Expand Down
11 changes: 11 additions & 0 deletions web/src/lib/languageModels/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,17 @@ export const modelSupportsImageInput = (
return modelConfiguration?.supports_image_input || false;
};

/** Display name for form-state model rows, which do not reliably carry
* effectiveDisplayName. Everything else should read that field instead. */
export function modelDisplayName(
model: Pick<
ModelConfiguration,
"name" | "display_name" | "custom_display_name"
>
): string {
return model.custom_display_name || model.display_name || model.name;
}

export function getDisplayName(
agent: MinimalAgent,
llmProviders: LLMProviderDescriptor[]
Expand Down
10 changes: 2 additions & 8 deletions web/src/sections/modals/languageModels/BedrockModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
useInitialValues,
buildValidationSchema,
BaseLLMFormValues,
mergeFetchedModelConfigurations,
withFetchedModels,
} from "@/sections/modals/languageModels/utils";
import { submitProvider } from "@/sections/modals/languageModels/svc";
import { LLMProviderConfiguredSource } from "@/lib/analytics/utils";
Expand Down Expand Up @@ -117,13 +117,7 @@ function BedrockModalInternals({
if (error) {
throw new Error(error);
}
formikProps.setFieldValue(
"model_configurations",
mergeFetchedModelConfigurations(
models,
formikProps.values.model_configurations
)
);
formikProps.setValues(withFetchedModels(models));
};

return (
Expand Down
10 changes: 2 additions & 8 deletions web/src/sections/modals/languageModels/BifrostModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
useInitialValues,
buildValidationSchema,
BaseLLMFormValues,
mergeFetchedModelConfigurations,
withFetchedModels,
} from "@/sections/modals/languageModels/utils";
import { submitProvider } from "@/sections/modals/languageModels/svc";
import { LLMProviderConfiguredSource } from "@/lib/analytics/utils";
Expand Down Expand Up @@ -83,13 +83,7 @@ function BifrostModalInternals({
if (error) {
throw new Error(error);
}
formikProps.setFieldValue(
"model_configurations",
mergeFetchedModelConfigurations(
models,
formikProps.values.model_configurations
)
);
formikProps.setValues(withFetchedModels(models));
};

return (
Expand Down
44 changes: 38 additions & 6 deletions web/src/sections/modals/languageModels/CustomModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import {
} from "@/lib/languageModels/types";
import type { ModelConfiguration } from "@/lib/languageModels/types";
import * as Yup from "yup";
import { useInitialValues } from "@/sections/modals/languageModels/utils";
import {
clampModelSettings,
useInitialValues,
} from "@/sections/modals/languageModels/utils";
import { submitProvider } from "@/sections/modals/languageModels/svc";
import { LLMProviderConfiguredSource } from "@/lib/analytics/utils";
import {
Expand All @@ -20,6 +23,7 @@ import {
ModalWrapper,
useApiBaseSubDescription,
} from "@/sections/modals/languageModels/shared";
import { ModelSettingsPopover } from "@/sections/modals/languageModels/ModelSettingsPopover";
import { useCustomProviderNames } from "@/lib/languageModels/hooks";
import InputTypeInField from "@/refresh-components/form/InputTypeInField";
import KeyValueInput, {
Expand All @@ -44,11 +48,19 @@ import { Section } from "@/layouts/general-layouts";

// ─── Model Configuration List ─────────────────────────────────────────────────

const MODEL_GRID_COLS = "grid-cols-[2fr_2fr_minmax(10rem,1fr)_1fr_2.25rem]";
const MODEL_GRID_COLS =
"grid-cols-[2fr_2fr_minmax(10rem,1fr)_1fr_2.25rem_2.25rem]";

type CustomModelConfiguration = Pick<
ModelConfiguration,
"name" | "max_input_tokens" | "supports_image_input"
| "name"
| "max_input_tokens"
| "supports_image_input"
| "supports_reasoning"
| "supported_reasoning_efforts"
| "reasoning_effort_max"
| "reasoning_effort_default"
| "temperature_default"
> & {
display_name: string;
};
Expand Down Expand Up @@ -102,6 +114,10 @@ function ModelConfigurationItem({
}
type="number"
/>
<ModelSettingsPopover
model={model}
onChange={(patch) => onChange({ ...model, ...patch })}
/>
<Button
disabled={!canRemove}
prominence="tertiary"
Expand Down Expand Up @@ -139,6 +155,7 @@ function ModelConfigurationList() {
display_name: "",
max_input_tokens: null,
supports_image_input: false,
supports_reasoning: false,
},
]);
}
Expand All @@ -154,6 +171,7 @@ function ModelConfigurationList() {
<Text mainUiAction>Input Type</Text>
<Text mainUiAction>Max Tokens</Text>
<div aria-hidden />
<div aria-hidden />

{models.map((model, index) => (
<ModelConfigurationItem
Expand Down Expand Up @@ -258,14 +276,20 @@ export default function CustomModal({
),
provider: existingLlmProvider?.provider ?? "",
api_version: existingLlmProvider?.api_version ?? "",
model_configurations: existingLlmProvider?.model_configurations.map(
(mc) => ({
model_configurations: existingLlmProvider?.model_configurations.map((mc) =>
// Stored policy can exceed a capability that shrank since the save,
// and the API rejects such values on submit.
clampModelSettings({
name: mc.name,
display_name: mc.display_name ?? "",
is_visible: mc.is_visible,
max_input_tokens: mc.max_input_tokens ?? null,
supports_image_input: mc.supports_image_input,
supports_reasoning: mc.supports_reasoning,
supported_reasoning_efforts: mc.supported_reasoning_efforts,
reasoning_effort_max: mc.reasoning_effort_max,
reasoning_effort_default: mc.reasoning_effort_default,
temperature_default: mc.temperature_default,
effectiveDisplayName: mc.effectiveDisplayName,
})
) ?? [
Expand All @@ -276,6 +300,10 @@ export default function CustomModal({
max_input_tokens: null,
supports_image_input: false,
supports_reasoning: false,
supported_reasoning_efforts: undefined,
reasoning_effort_max: null,
reasoning_effort_default: null,
temperature_default: null,
effectiveDisplayName: "",
},
],
Expand Down Expand Up @@ -326,7 +354,11 @@ export default function CustomModal({
is_visible: true,
max_input_tokens: mc.max_input_tokens ?? null,
supports_image_input: mc.supports_image_input,
supports_reasoning: false,
supports_reasoning: mc.supports_reasoning,
supported_reasoning_efforts: mc.supported_reasoning_efforts,
reasoning_effort_max: mc.reasoning_effort_max,
reasoning_effort_default: mc.reasoning_effort_default,
temperature_default: mc.temperature_default,
effectiveDisplayName: mc.display_name || mc.name,
}));

Expand Down
10 changes: 2 additions & 8 deletions web/src/sections/modals/languageModels/LMStudioModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
useInitialValues,
buildValidationSchema,
BaseLLMFormValues as BaseLLMModalValues,
mergeFetchedModelConfigurations,
withFetchedModels,
} from "@/sections/modals/languageModels/utils";
import { submitProvider } from "@/sections/modals/languageModels/svc";
import { LLMProviderConfiguredSource } from "@/lib/analytics/utils";
Expand Down Expand Up @@ -64,13 +64,7 @@ function LMStudioModalInternals({
if (data.error) {
throw new Error(data.error);
}
formikProps.setFieldValue(
"model_configurations",
mergeFetchedModelConfigurations(
data.models,
formikProps.values.model_configurations
)
);
formikProps.setValues(withFetchedModels(data.models));
};

return (
Expand Down
10 changes: 2 additions & 8 deletions web/src/sections/modals/languageModels/LiteLLMProxyModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
useInitialValues,
buildValidationSchema,
BaseLLMFormValues,
mergeFetchedModelConfigurations,
withFetchedModels,
} from "@/sections/modals/languageModels/utils";
import { submitProvider } from "@/sections/modals/languageModels/svc";
import { LLMProviderConfiguredSource } from "@/lib/analytics/utils";
Expand Down Expand Up @@ -57,13 +57,7 @@ function LiteLLMProxyModalInternals({
if (error) {
throw new Error(error);
}
formikProps.setFieldValue(
"model_configurations",
mergeFetchedModelConfigurations(
models,
formikProps.values.model_configurations
)
);
formikProps.setValues(withFetchedModels(models));
};

return (
Expand Down
Loading
Loading