Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 12 additions & 3 deletions apps/web/src/domains/settings/ai/ai-types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import { PROVIDER_DISPLAY_NAMES } from "@/assistant/llm-model-catalog";
import type { CallSiteOverrideDraft } from "@/domains/settings/ai/call-site-overrides-modal";

// ---------------------------------------------------------------------------
// Call-site override draft
// ---------------------------------------------------------------------------

export interface CallSiteOverrideDraft {
profile?: string | null;
provider?: string | null;
model?: string | null;
}

// ---------------------------------------------------------------------------
// Service mode
Expand Down Expand Up @@ -37,6 +46,8 @@ export interface ProfileEntry {
contextWindow?: { maxInputTokens?: number };
}

export type ProfileWithName = { name: string } & ProfileEntry;

export interface DaemonConfig {
services?: {
"web-search"?: { mode?: string; provider?: string };
Expand All @@ -59,8 +70,6 @@ export interface DaemonConfigReconciliation {
imageGenMode?: ServiceMode;
}

export type { CallSiteOverrideDraft };

export interface InferenceTokenBudgetState {
maxOutputTokens: number;
maxOutputTouched: boolean;
Expand Down
23 changes: 23 additions & 0 deletions apps/web/src/domains/settings/ai/ai-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,33 @@ import type {
DaemonConfig,
DaemonConfigReconciliation,
InferenceTokenBudgetState,
ProfileEntry,
ProfileWithName,
ServiceMode,
} from "@/domains/settings/ai/ai-types";
import { TOKEN_SLIDER_MIN_TOKENS } from "@/domains/settings/ai/ai-types";

/**
* Merges `profileOrder` with `profiles` to produce a stable ordered list.
*
* Entries appear in `profileOrder` sequence first, followed by any extras
* present in `profiles` but missing from `profileOrder` (e.g. newly seeded
* profiles that haven't been reordered yet).
*/
export function buildOrderedProfiles(
profiles: Record<string, ProfileEntry>,
profileOrder: string[],
): ProfileWithName[] {
const ordered = profileOrder
.filter((name) => name in profiles)
.map((name) => ({ name, ...profiles[name]! }));
const inOrder = new Set(profileOrder);
const extras = Object.entries(profiles)
.filter(([name]) => !inOrder.has(name))
.map(([name, entry]) => ({ name, ...entry }));
return [...ordered, ...extras];
}

export function assertProvisionSuccess(result: unknown): void {
if (
result &&
Expand Down
25 changes: 3 additions & 22 deletions apps/web/src/domains/settings/ai/call-site-overrides-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
} from "@/assistant/llm-model-catalog";

import {
type CallSiteOverrideDraft,
INFERENCE_PROVIDER_DISPLAY_NAMES,
INFERENCE_PROVIDERS,
} from "@/domains/settings/ai/ai-types";
Expand All @@ -28,7 +29,6 @@ import {
visibleProfilesForPicker,
gateAutoProfile,
selectSeedProfileForOverride,
type ProfilePickerEntry,
} from "@/domains/settings/ai/profile-pickers";
import {
useDaemonConfig,
Expand All @@ -39,7 +39,7 @@ import {
// Sentinel value for the "Custom" profile picker option
// ---------------------------------------------------------------------------

export const CUSTOM_SENTINEL = "__custom__";
const CUSTOM_SENTINEL = "__custom__";

// ---------------------------------------------------------------------------
// Types
Expand All @@ -49,12 +49,6 @@ type CallSiteCatalog = ConfigLlmCallsitesGetResponse;
type CallSiteEntry = CallSiteCatalog["callSites"][number];
type CallSiteDomain = CallSiteCatalog["domains"][number];

export interface CallSiteOverrideDraft {
profile?: string | null;
provider?: string | null;
model?: string | null;
}

export interface CallSiteOverridesModalProps {
isOpen: boolean;
onClose: () => void;
Expand Down Expand Up @@ -131,8 +125,7 @@ function CallSiteOverridesModalInner({
onSavingChange,
}: InnerProps) {
const {
profiles,
profileOrder,
orderedProfiles,
callSites: persistedOverrides,
config: daemonConfig,
} = useDaemonConfig();
Expand Down Expand Up @@ -176,18 +169,6 @@ function CallSiteOverridesModalInner({
return all.filter((cs) => cs.id !== "analyzeConversation");
}, [catalog, analyzeConversationEnabled]);

// Build ordered profile list from cache slices
const orderedProfiles: ReadonlyArray<ProfilePickerEntry> = useMemo(() => {
const ordered = profileOrder
.filter((name) => name in profiles)
.map((name) => ({ name, ...profiles[name]! }));
const inOrder = new Set(profileOrder);
const extras = Object.entries(profiles)
.filter(([name]) => !inOrder.has(name))
.map(([name, entry]) => ({ name, ...entry }));
return [...ordered, ...extras];
}, [profiles, profileOrder]);

const daemonConfigLoaded = !!daemonConfig;

// Seed drafts once per open, but defer until BOTH the catalog and the daemon
Expand Down
15 changes: 1 addition & 14 deletions apps/web/src/domains/settings/ai/language-model-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@ import {
export function LanguageModelCard() {
const {
assistantId,
profiles,
profileOrder,
orderedProfiles,
activeProfile,
callSites,
patchDaemonConfig,
Expand Down Expand Up @@ -55,18 +54,6 @@ export function LanguageModelCard() {
const [overridesOpen, setOverridesOpen] = useState(false);
const [manageProvidersOpen, setManageProvidersOpen] = useState(false);

// Derived state — computed from query cache slices
const orderedProfiles = useMemo(() => {
const ordered = profileOrder
.filter((name) => name in profiles)
.map((name) => ({ name, ...profiles[name]! }));
const inOrder = new Set(profileOrder);
const extras = Object.entries(profiles)
.filter(([name]) => !inOrder.has(name))
.map(([name, entry]) => ({ name, ...entry }));
return [...ordered, ...extras];
}, [profiles, profileOrder]);

const queryComplexityRoutingEnabled =
useAssistantFeatureFlagStore.use.queryComplexityRouting();

Expand Down
72 changes: 12 additions & 60 deletions apps/web/src/domains/settings/ai/manage-profiles-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { Tag } from "@vellum/design-library/components/tag";
import { Typography } from "@vellum/design-library/components/typography";
import { useAssistantFeatureFlagStore } from "@/stores/assistant-feature-flag-store";

import type { DaemonConfig, ProfileEntry } from "@/domains/settings/ai/ai-types";
import type { DaemonConfig, ProfileEntry, ProfileWithName } from "@/domains/settings/ai/ai-types";
import { ProfileEditorModal } from "@/domains/settings/ai/profile-editor-modal";
import {
AUTO_PROFILE_NAME,
Expand All @@ -25,24 +25,6 @@ import { assistantDaemonConfigQueryKey } from "@/lib/sync/query-tags";
// Types
// ---------------------------------------------------------------------------

export interface Profile {
name: string;
source?: "managed" | "user";
status?: "active" | "disabled";
label?: string | null;
description?: string | null;
provider?: string | null;
provider_connection?: string | null;
model?: string | null;
maxTokens?: number;
effort?: string;
speed?: string;
verbosity?: string;
temperature?: number | null;
thinking?: { enabled?: boolean; streamThinking?: boolean; level?: string };
contextWindow?: { maxInputTokens?: number };
}

interface BlockedDeleteState {
name: string;
label: string;
Expand All @@ -56,30 +38,6 @@ interface ManageProfilesModalProps {
onClose: () => void;
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

function profileEntryToProfile(name: string, entry: ProfileEntry): Profile {
return {
name,
source: entry.source,
status: entry.status ?? "active",
label: entry.label ?? undefined,
description: entry.description ?? undefined,
provider: entry.provider ?? undefined,
provider_connection: entry.provider_connection ?? undefined,
model: entry.model ?? undefined,
maxTokens: entry.maxTokens,
effort: entry.effort,
speed: entry.speed,
verbosity: entry.verbosity,
temperature: entry.temperature,
thinking: entry.thinking,
contextWindow: entry.contextWindow,
};
}

// ---------------------------------------------------------------------------
// ManageProfilesModal
// ---------------------------------------------------------------------------
Expand All @@ -92,14 +50,15 @@ export function ManageProfilesModal({
const {
profiles,
profileOrder,
orderedProfiles,
activeProfile,
callSites,
} = useDaemonConfig();
const configMutation = useDaemonConfigMutation();

const openAICompatibleEndpoints = useAssistantFeatureFlagStore.use.openAICompatibleEndpoints();
const [editorOpen, setEditorOpen] = useState(false);
const [editingProfile, setEditingProfile] = useState<Profile | null>(null);
const [editingProfile, setEditingProfile] = useState<ProfileWithName | null>(null);

// Provider connections — shared TanStack Query cache with ManageProvidersModal.
const { data: connectionsData } = useQuery({
Expand Down Expand Up @@ -186,6 +145,7 @@ export function ManageProfilesModal({
<ManageProfilesModalInner
profiles={profiles}
profileOrder={profileOrder}
orderedProfiles={orderedProfiles}
activeProfile={activeProfile}
assistantId={assistantId}
callSiteOverrides={callSites}
Expand Down Expand Up @@ -232,17 +192,19 @@ export function ManageProfilesModal({
interface ManageProfilesModalInnerProps {
profiles: Record<string, ProfileEntry>;
profileOrder: string[];
orderedProfiles: ProfileWithName[];
activeProfile: string | null;
assistantId: string;
callSiteOverrides: Record<string, { profile?: string | null } | null | undefined>;
onClose: () => void;
onEditClick: (profile: Profile) => void;
onEditClick: (profile: ProfileWithName) => void;
onNewClick: () => void;
}

function ManageProfilesModalInner({
profiles,
profileOrder,
orderedProfiles,
activeProfile,
assistantId,
callSiteOverrides,
Expand Down Expand Up @@ -276,22 +238,12 @@ function ManageProfilesModalInner({
const queryComplexityRouting = useAssistantFeatureFlagStore.use.queryComplexityRouting();

// Build ordered profile list
const allOrderedProfiles: Profile[] = useMemo(() => {
const ordered = profileOrder
.filter((name) => name in profiles)
.map((name) => profileEntryToProfile(name, profiles[name]!));
const inOrder = new Set(profileOrder);
const extras = Object.entries(profiles)
.filter(([name]) => !inOrder.has(name))
.map(([name, entry]) => profileEntryToProfile(name, entry));
return gateAutoProfile(
[...ordered, ...extras],
queryComplexityRouting,
);
}, [profiles, profileOrder, queryComplexityRouting]);
const allOrderedProfiles: ProfileWithName[] = useMemo(() => {
return gateAutoProfile(orderedProfiles, queryComplexityRouting);
}, [orderedProfiles, queryComplexityRouting]);

async function handleStatusToggle(
profile: Profile,
profile: ProfileWithName,
active: boolean,
): Promise<boolean> {
if (togglingNames.has(profile.name)) return false;
Expand Down Expand Up @@ -745,7 +697,7 @@ function BlockedDeleteModal({
onConfirm,
}: {
blocked: BlockedDeleteState | null;
availableReplacements: Profile[];
availableReplacements: ProfileWithName[];
replacement: string;
onReplacementChange: (value: string) => void;
error: string | null;
Expand Down
7 changes: 3 additions & 4 deletions apps/web/src/domains/settings/ai/profile-editor-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ import {
PROVIDER_DISPLAY_NAMES as INFERENCE_PROVIDER_DISPLAY_NAMES,
} from "@/assistant/llm-model-catalog";

import type { ProfileEntry } from "@/domains/settings/ai/ai-types";
import { type Profile } from "@/domains/settings/ai/manage-profiles-modal";
import type { ProfileEntry, ProfileWithName } from "@/domains/settings/ai/ai-types";
import {
ProfileAdvancedParams,
THINKING_LEVEL_INHERIT,
Expand Down Expand Up @@ -50,7 +49,7 @@ export interface ProfileEditorModalProps {
isOpen: boolean;
mode: "create" | "edit" | "view";
profileName?: string;
initialValues?: Profile;
initialValues?: ProfileWithName;
existingNames: string[];
/**
* Provider connections, supplied by the parent (`ManageProfilesModal`).
Expand Down Expand Up @@ -133,7 +132,7 @@ export function ProfileEditorModal({
interface ProfileEditorModalInnerProps {
mode: "create" | "edit" | "view";
profileName?: string;
initialValues?: Profile;
initialValues?: ProfileWithName;
existingNames: string[];
// See `ProfileEditorModalProps.connections` for nil-vs-empty semantics.
connections: ProviderConnection[] | undefined;
Expand Down
Loading