feat: add config v2 to simplify billing (#428)

* feat: add model config v2

* chore: centralize user org selection

* chore: move preferences to platform settings

* fix: decouple org preference and ai model preferences
This commit is contained in:
Abhishek 2026-06-09 16:10:26 +05:30 committed by GitHub
parent 49e68b49d5
commit cdbd06c8d9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
42 changed files with 5135 additions and 264 deletions

View file

@ -1,13 +1,25 @@
import ServiceConfiguration from "@/components/ServiceConfiguration";
import ModelConfigurationV2 from "@/components/ModelConfigurationV2";
import { SETTINGS_DOCUMENTATION_URLS } from "@/constants/documentation";
export default function ServiceConfigurationPage() {
interface ServiceConfigurationPageProps {
searchParams?: Promise<{
action?: string | string[];
}>;
}
export default async function ServiceConfigurationPage({ searchParams }: ServiceConfigurationPageProps) {
const params = searchParams ? await searchParams : {};
const action = Array.isArray(params.action) ? params.action[0] : params.action;
return (
<div className="min-h-screen bg-background">
<div className="container mx-auto px-4 py-8">
<div className="max-w-4xl mx-auto">
<ServiceConfiguration docsUrl={SETTINGS_DOCUMENTATION_URLS.modelOverrides} />
<ModelConfigurationV2
docsUrl={SETTINGS_DOCUMENTATION_URLS.modelOverrides}
initialAction={action}
/>
</div>
</div>
</div>

View file

@ -7,6 +7,7 @@ import { useEffect,useState } from 'react';
import {
getDailyReportApiV1OrganizationsReportsDailyGet,
getDailyRunsDetailApiV1OrganizationsReportsDailyRunsGet,
getPreferencesApiV1OrganizationsPreferencesGet,
getWorkflowOptionsApiV1OrganizationsReportsWorkflowsGet
} from '@/client/sdk.gen';
import type { WorkflowRunDetail } from '@/client/types.gen';
@ -16,7 +17,6 @@ import { Card } from '@/components/ui/card';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Skeleton } from '@/components/ui/skeleton';
import { useUserConfig } from '@/context/UserConfigContext';
import { useAuth } from '@/lib/auth';
import { DispositionChart } from './components/DispositionChart';
@ -57,11 +57,9 @@ export default function ReportsPage() {
const [report, setReport] = useState<DailyReport | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const { userConfig } = useUserConfig();
const [timezone, setTimezone] = useState('America/New_York');
const auth = useAuth();
const timezone = userConfig?.timezone || 'America/New_York';
// Fetch workflows on mount
useEffect(() => {
const fetchWorkflows = async () => {
@ -80,6 +78,22 @@ export default function ReportsPage() {
fetchWorkflows();
}, [auth.isAuthenticated]);
useEffect(() => {
const fetchPreferences = async () => {
if (!auth.isAuthenticated) return;
try {
const response = await getPreferencesApiV1OrganizationsPreferencesGet();
if (response.data?.timezone) {
setTimezone(response.data.timezone);
}
} catch (err) {
console.error('Failed to fetch organization preferences:', err);
}
};
fetchPreferences();
}, [auth.isAuthenticated]);
// Fetch report data when date or workflow changes
useEffect(() => {
const fetchReport = async () => {

View file

@ -3,6 +3,7 @@
import { ExternalLink } from "lucide-react";
import { MCPSection } from "@/components/MCPSection";
import { OrganizationPreferencesSection } from "@/components/OrganizationPreferencesSection";
import { TelemetrySection } from "@/components/TelemetrySection";
import {
Card,
@ -23,6 +24,19 @@ export default function SettingsPage() {
</p>
</div>
<Card>
<CardHeader>
<CardTitle>Preferences</CardTitle>
<CardDescription>
Set organization-wide defaults such as the test phone number and
timezone.
</CardDescription>
</CardHeader>
<CardContent>
<OrganizationPreferencesSection />
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>MCP Server</CardTitle>

View file

@ -6,8 +6,8 @@ import { useCallback, useEffect, useId, useState } from 'react';
import TimezoneSelect, { type ITimezoneOption } from 'react-timezone-select';
import { toast } from 'sonner';
import { downloadUsageRunsReportApiV1OrganizationsUsageRunsReportGet, getDailyUsageBreakdownApiV1OrganizationsUsageDailyBreakdownGet, getMpsCreditsApiV1OrganizationsUsageMpsCreditsGet, getUsageHistoryApiV1OrganizationsUsageRunsGet } from '@/client/sdk.gen';
import type { DailyUsageBreakdownResponse, MpsCreditsResponse, UsageHistoryResponse, WorkflowRunUsageResponse } from '@/client/types.gen';
import { downloadUsageRunsReportApiV1OrganizationsUsageRunsReportGet, getDailyUsageBreakdownApiV1OrganizationsUsageDailyBreakdownGet, getMpsCreditsApiV1OrganizationsUsageMpsCreditsGet, getPreferencesApiV1OrganizationsPreferencesGet, getUsageHistoryApiV1OrganizationsUsageRunsGet, savePreferencesApiV1OrganizationsPreferencesPut } from '@/client/sdk.gen';
import type { DailyUsageBreakdownResponse, MpsCreditsResponse, OrganizationPreferences, UsageHistoryResponse, WorkflowRunUsageResponse } from '@/client/types.gen';
import { CallTypeCell } from '@/components/CallTypeCell';
import { DailyUsageTable } from '@/components/DailyUsageTable';
import { FilterBuilder } from '@/components/filters/FilterBuilder';
@ -36,7 +36,7 @@ const getLocalTimezone = () => Intl.DateTimeFormat().resolvedOptions().timeZone;
export default function UsagePage() {
const router = useRouter();
const searchParams = useSearchParams();
const { userConfig, saveUserConfig, loading: userConfigLoading, organizationPricing } = useUserConfig();
const { organizationPricing } = useUserConfig();
const auth = useAuth();
// MPS credits state
@ -74,6 +74,8 @@ export default function UsagePage() {
const localTimezone = getLocalTimezone();
const [selectedTimezone, setSelectedTimezone] = useState<ITimezoneOption | string>('');
const [savingTimezone, setSavingTimezone] = useState(false);
const [preferences, setPreferences] = useState<OrganizationPreferences>({});
const [preferencesLoading, setPreferencesLoading] = useState(true);
const timezoneSelectId = useId(); // Stable ID for react-select to prevent hydration mismatch
// Fetch MPS credits
@ -168,6 +170,23 @@ export default function UsagePage() {
}
}, [auth.isAuthenticated, organizationPricing]);
const fetchPreferences = useCallback(async () => {
if (!auth.isAuthenticated) return;
setPreferencesLoading(true);
try {
const response = await getPreferencesApiV1OrganizationsPreferencesGet();
const nextPreferences = response.data || {};
setPreferences(nextPreferences);
setSelectedTimezone(nextPreferences.timezone || localTimezone);
} catch (error) {
console.error('Failed to fetch organization preferences:', error);
setSelectedTimezone(localTimezone);
} finally {
setPreferencesLoading(false);
}
}, [auth.isAuthenticated, localTimezone]);
// Download a CSV of all runs matching the current filters.
const handleDownloadReport = async () => {
if (!auth.isAuthenticated) return;
@ -203,31 +222,31 @@ export default function UsagePage() {
const handleTimezoneChange = async (timezone: ITimezoneOption | string) => {
setSelectedTimezone(timezone);
setSavingTimezone(true);
const previousTimezone = preferences.timezone || localTimezone;
try {
const tzValue = typeof timezone === 'string' ? timezone : timezone.value;
await saveUserConfig({ timezone: tzValue });
const response = await savePreferencesApiV1OrganizationsPreferencesPut({
body: {
...preferences,
timezone: tzValue,
},
});
if (response.error) {
throw new Error('Failed to save timezone');
}
setPreferences(response.data || { ...preferences, timezone: tzValue });
} catch (error) {
console.error('Failed to save timezone:', error);
// Revert to previous timezone on error
const prevTz = userConfig?.timezone || localTimezone;
setSelectedTimezone(prevTz);
setSelectedTimezone(previousTimezone);
} finally {
setSavingTimezone(false);
}
};
// Update timezone when userConfig loads
// Update timezone when organization preferences load.
useEffect(() => {
if (!userConfigLoading) {
// Config has loaded - set the timezone
if (userConfig?.timezone) {
setSelectedTimezone(userConfig.timezone);
} else {
// No saved timezone, use local
setSelectedTimezone(localTimezone);
}
}
}, [userConfig, userConfigLoading, localTimezone]);
fetchPreferences();
}, [fetchPreferences]);
// Initial load - fetch when auth becomes available
useEffect(() => {
@ -340,8 +359,8 @@ export default function UsagePage() {
instanceId={timezoneSelectId}
value={selectedTimezone}
onChange={handleTimezoneChange}
isDisabled={savingTimezone || userConfigLoading}
placeholder={userConfigLoading ? "Loading..." : "Select timezone"}
isDisabled={savingTimezone || preferencesLoading}
placeholder={preferencesLoading ? "Loading..." : "Select timezone"}
styles={{
control: (base, state) => ({
...base,

View file

@ -4,15 +4,21 @@ import 'react-international-phone/style.css';
import { Loader2 } from "lucide-react";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import { PhoneInput } from 'react-international-phone';
import {
getPreferencesApiV1OrganizationsPreferencesGet,
initiateCallApiV1TelephonyInitiateCallPost,
listPhoneNumbersApiV1OrganizationsTelephonyConfigsConfigIdPhoneNumbersGet,
listTelephonyConfigurationsApiV1OrganizationsTelephonyConfigsGet
listTelephonyConfigurationsApiV1OrganizationsTelephonyConfigsGet,
savePreferencesApiV1OrganizationsPreferencesPut,
} from '@/client/sdk.gen';
import type { PhoneNumberResponse, TelephonyConfigurationListItem } from '@/client/types.gen';
import type {
OrganizationPreferences,
PhoneNumberResponse,
TelephonyConfigurationListItem,
} from '@/client/types.gen';
import { Button } from "@/components/ui/button";
import {
Dialog,
@ -33,6 +39,7 @@ import {
SelectValue,
} from "@/components/ui/select";
import { useUserConfig } from "@/context/UserConfigContext";
import { detailFromError } from "@/lib/apiError";
interface PhoneCallDialogProps {
open: boolean;
@ -48,21 +55,40 @@ export const PhoneCallDialog = ({
user,
}: PhoneCallDialogProps) => {
const router = useRouter();
const { userConfig, saveUserConfig } = useUserConfig();
const [phoneNumber, setPhoneNumber] = useState(userConfig?.test_phone_number || "");
const { refreshConfig } = useUserConfig();
const [preferences, setPreferences] = useState<OrganizationPreferences>({});
const [preferencesLoaded, setPreferencesLoaded] = useState(false);
const [phoneNumber, setPhoneNumber] = useState("");
const [callLoading, setCallLoading] = useState(false);
const [callError, setCallError] = useState<string | null>(null);
const [callSuccessMsg, setCallSuccessMsg] = useState<string | null>(null);
const [phoneChanged, setPhoneChanged] = useState(false);
const [checkingConfig, setCheckingConfig] = useState(false);
const [needsConfiguration, setNeedsConfiguration] = useState<boolean | null>(null);
const [sipMode, setSipMode] = useState(() => /^(PJSIP|SIP)\//i.test(userConfig?.test_phone_number || ""));
const [sipMode, setSipMode] = useState(false);
const [telephonyConfigs, setTelephonyConfigs] = useState<TelephonyConfigurationListItem[]>([]);
const [selectedConfigId, setSelectedConfigId] = useState<string>("");
const [fromPhoneNumbers, setFromPhoneNumbers] = useState<PhoneNumberResponse[]>([]);
const [selectedFromPhoneNumberId, setSelectedFromPhoneNumberId] = useState<string>("");
const [loadingPhoneNumbers, setLoadingPhoneNumbers] = useState(false);
const fetchPreferences = useCallback(async () => {
const result =
await getPreferencesApiV1OrganizationsPreferencesGet();
if (result.error) {
throw new Error(detailFromError(result.error, "Failed to load phone preferences"));
}
return result.data || {};
}, []);
const applyPreferences = useCallback((nextPreferences: OrganizationPreferences) => {
const saved = nextPreferences.test_phone_number || "";
setPreferences(nextPreferences);
setPhoneNumber(saved);
setSipMode(/^(PJSIP|SIP)\//i.test(saved));
setPhoneChanged(false);
}, []);
// Check telephony configuration when dialog opens
useEffect(() => {
const checkConfig = async () => {
@ -97,6 +123,33 @@ export const PhoneCallDialog = ({
checkConfig();
}, [open]);
// Load organization-scoped call preferences when dialog opens.
useEffect(() => {
if (!open) return;
let cancelled = false;
setPreferencesLoaded(false);
const loadPreferences = async () => {
try {
const nextPreferences = await fetchPreferences();
if (cancelled) return;
applyPreferences(nextPreferences);
setPreferencesLoaded(true);
} catch (err) {
if (cancelled) return;
applyPreferences({});
setPreferencesLoaded(false);
setCallError(err instanceof Error ? err.message : "Failed to load phone preferences");
}
};
loadPreferences();
return () => {
cancelled = true;
};
}, [applyPreferences, fetchPreferences, open]);
// Reset state when dialog closes
useEffect(() => {
if (!open) {
@ -149,22 +202,9 @@ export const PhoneCallDialog = ({
};
}, [open, selectedConfigId]);
// Keep phoneNumber in sync with userConfig when dialog opens
useEffect(() => {
if (open) {
const saved = userConfig?.test_phone_number || "";
setPhoneNumber(saved);
setSipMode(/^(PJSIP|SIP)\//i.test(saved));
setPhoneChanged(false);
setCallError(null);
setCallSuccessMsg(null);
setCallLoading(false);
}
}, [open, userConfig?.test_phone_number]);
const handlePhoneInputChange = (formattedValue: string) => {
setPhoneNumber(formattedValue);
setPhoneChanged(formattedValue !== userConfig?.test_phone_number);
setPhoneChanged(formattedValue !== (preferences.test_phone_number || ""));
setCallError(null);
setCallSuccessMsg(null);
};
@ -174,17 +214,39 @@ export const PhoneCallDialog = ({
router.push('/telephony-configurations');
};
const savePhoneNumberPreference = async () => {
const currentPreferences = preferencesLoaded ? preferences : await fetchPreferences();
const result =
await savePreferencesApiV1OrganizationsPreferencesPut({
body: {
...currentPreferences,
test_phone_number: phoneNumber || null,
},
});
if (result.error) {
throw new Error(detailFromError(result.error, "Failed to save phone preferences"));
}
if (!result.data) {
throw new Error("Failed to save phone preferences");
}
setPreferences(result.data);
setPreferencesLoaded(true);
setPhoneChanged(false);
await refreshConfig();
};
const handleStartCall = async () => {
setCallLoading(true);
setCallError(null);
setCallSuccessMsg(null);
try {
if (!user || !userConfig) return;
if (!user) return;
// Save phone number if it has changed
if (phoneChanged) {
await saveUserConfig({ ...userConfig, test_phone_number: phoneNumber });
setPhoneChanged(false);
await savePhoneNumberPreference();
}
const response = await initiateCallApiV1TelephonyInitiateCallPost({

View file

@ -7,8 +7,22 @@ import { useParams, useRouter } from "next/navigation";
import { useEffect, useMemo, useRef, useState } from "react";
import { toast } from "sonner";
import { downloadWorkflowReportApiV1WorkflowWorkflowIdReportGet, getAmbientNoiseUploadUrlApiV1WorkflowAmbientNoiseUploadUrlPost, getWorkflowApiV1WorkflowFetchWorkflowIdGet } from "@/client/sdk.gen";
import type { WorkflowResponse } from "@/client/types.gen";
import {
downloadWorkflowReportApiV1WorkflowWorkflowIdReportGet,
getAmbientNoiseUploadUrlApiV1WorkflowAmbientNoiseUploadUrlPost,
getModelConfigurationV2ApiV1OrganizationsModelConfigurationsV2Get,
getModelConfigurationV2DefaultsApiV1OrganizationsModelConfigurationsV2DefaultsGet,
getWorkflowApiV1WorkflowFetchWorkflowIdGet,
} from "@/client/sdk.gen";
import type {
OrganizationAiModelConfigurationResponse,
OrganizationAiModelConfigurationV2,
WorkflowResponse,
} from "@/client/types.gen";
import {
AIModelConfigurationV2Editor,
type ModelConfigurationDefaultsV2,
} from "@/components/AIModelConfigurationV2Editor";
import { FlowEdge, FlowNode } from "@/components/flow/types";
import { LLMConfigSelector } from "@/components/LLMConfigSelector";
import { ServiceConfigurationForm } from "@/components/ServiceConfigurationForm";
@ -26,6 +40,7 @@ import { Textarea } from "@/components/ui/textarea";
import { SETTINGS_DOCUMENTATION_URLS } from "@/constants/documentation";
import { UnsavedChangesProvider, useUnsavedChanges, useUnsavedChangesContext } from "@/context/UnsavedChangesContext";
import { useAudioPlayback } from "@/hooks/useAudioPlayback";
import { detailFromError } from "@/lib/apiError";
import { useAuth } from "@/lib/auth";
import logger from "@/lib/logger";
import {
@ -1040,6 +1055,182 @@ function AgentUuidSection({ workflowUuid }: { workflowUuid: string }) {
);
}
// ---------------------------------------------------------------------------
// Section: Model Overrides
// ---------------------------------------------------------------------------
function withoutModelConfigurationOverrides(configurations: WorkflowConfigurations): WorkflowConfigurations {
const next = { ...configurations };
delete next.model_overrides;
delete next.model_configuration_v2_override;
return next;
}
function WorkflowModelOverridesSection({
workflowConfigurations,
workflowName,
onSave,
modelConfigurationDefaults,
organizationModelConfiguration,
modelConfigurationLoading,
modelConfigurationError,
}: {
workflowConfigurations: WorkflowConfigurations;
workflowName: string;
onSave: (configurations: WorkflowConfigurations, workflowName: string) => Promise<void>;
modelConfigurationDefaults: ModelConfigurationDefaultsV2 | null;
organizationModelConfiguration: OrganizationAiModelConfigurationResponse | null;
modelConfigurationLoading: boolean;
modelConfigurationError: string | null;
}) {
const savedV2Override = workflowConfigurations.model_configuration_v2_override;
const hasSavedModelOverride = Boolean(savedV2Override || workflowConfigurations.model_overrides);
const [overrideEnabled, setOverrideEnabled] = useState(Boolean(savedV2Override));
const [isRemovingOverride, setIsRemovingOverride] = useState(false);
useEffect(() => {
setOverrideEnabled(Boolean(workflowConfigurations.model_configuration_v2_override));
}, [workflowConfigurations.model_configuration_v2_override]);
const source = organizationModelConfiguration?.source || "empty";
const isV2 = source === "organization_v2";
const saveLegacyOverrides = async (config: Record<string, unknown>) => {
const nextConfigurations = withoutModelConfigurationOverrides(workflowConfigurations);
const modelOverrides = config.model_overrides as WorkflowConfigurations["model_overrides"] | undefined;
if (modelOverrides) {
nextConfigurations.model_overrides = modelOverrides;
}
await onSave(nextConfigurations, workflowName);
};
const saveV2Override = async (configuration: OrganizationAiModelConfigurationV2) => {
const nextConfigurations = withoutModelConfigurationOverrides(workflowConfigurations);
nextConfigurations.model_configuration_v2_override = configuration;
await onSave(nextConfigurations, workflowName);
toast.success("Model override saved");
};
const removeV2Override = async () => {
setIsRemovingOverride(true);
try {
await onSave(withoutModelConfigurationOverrides(workflowConfigurations), workflowName);
setOverrideEnabled(false);
toast.success("Using organization model configuration");
} finally {
setIsRemovingOverride(false);
}
};
return (
<Card id="models">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<Brain className="h-4 w-4" />
Model Overrides
</CardTitle>
<CardDescription>
{isV2
? "Override the full organization model configuration for this workflow."
: "Override global model settings for this workflow. Toggle individual services to customize."}{" "}
<a href={SETTINGS_DOCUMENTATION_URLS.modelOverrides} target="_blank" rel="noopener noreferrer" className="inline-flex items-center gap-0.5 underline">Learn more <ExternalLink className="h-3 w-3" /></a>
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{modelConfigurationLoading && (
<div className="flex items-center gap-2 rounded-md border p-4 text-sm text-muted-foreground">
<Loader2 className="h-4 w-4 animate-spin" />
Loading model configuration
</div>
)}
{modelConfigurationError && (
<div className="rounded-md border border-destructive/40 bg-destructive/10 px-4 py-3 text-sm text-destructive">
{modelConfigurationError}
</div>
)}
{!modelConfigurationLoading && !modelConfigurationError && !isV2 && (
<>
{source === "legacy_user_v1" && (
<div className="flex flex-col gap-3 rounded-md border bg-muted/30 p-4 sm:flex-row sm:items-center sm:justify-between">
<p className="text-sm text-muted-foreground">
This workflow is using legacy model overrides. Migrate organization model configuration to use v2 overrides.
</p>
<Button type="button" variant="outline" size="sm" asChild>
<Link href="/model-configurations?action=migrate_to_v2">Migrate to v2</Link>
</Button>
</div>
)}
<ServiceConfigurationForm
mode="override"
currentOverrides={workflowConfigurations.model_overrides}
submitLabel="Save Model Overrides"
onSave={saveLegacyOverrides}
/>
</>
)}
{!modelConfigurationLoading && !modelConfigurationError && isV2 && modelConfigurationDefaults && organizationModelConfiguration && (
<>
<div className="flex items-center justify-between rounded-md border p-4">
<div className="space-y-0.5">
<Label htmlFor="workflow-model-v2-override" className="text-sm font-medium">
Override for this workflow
</Label>
<p className="text-xs text-muted-foreground">
{overrideEnabled
? "This workflow uses its own complete model configuration."
: "This workflow uses the organization model configuration."}
</p>
</div>
<Switch
id="workflow-model-v2-override"
checked={overrideEnabled}
onCheckedChange={setOverrideEnabled}
/>
</div>
{overrideEnabled ? (
<AIModelConfigurationV2Editor
defaults={modelConfigurationDefaults}
configuration={
(savedV2Override as OrganizationAiModelConfigurationV2 | undefined)
|| (organizationModelConfiguration.configuration as OrganizationAiModelConfigurationV2 | null)
}
effectiveConfiguration={
savedV2Override
? null
: organizationModelConfiguration.effective_configuration
}
submitLabel="Save Model Override"
onSave={saveV2Override}
/>
) : (
<div className="rounded-md border bg-muted/20 p-4">
<p className="text-sm text-muted-foreground">
Using organization model configuration.
</p>
{hasSavedModelOverride && (
<Button
type="button"
variant="outline"
className="mt-3"
onClick={removeV2Override}
disabled={isRemovingOverride}
>
{isRemovingOverride ? "Saving..." : "Save Organization Configuration"}
</Button>
)}
</div>
)}
</>
)}
</CardContent>
</Card>
);
}
// ---------------------------------------------------------------------------
// Main Page
// ---------------------------------------------------------------------------
@ -1127,6 +1318,11 @@ function WorkflowSettingsInner({
const [isEmbedDialogOpen, setIsEmbedDialogOpen] = useState(false);
const [activeSection, setActiveSection] = useState("general");
const [modelConfigurationDefaults, setModelConfigurationDefaults] = useState<ModelConfigurationDefaultsV2 | null>(null);
const [organizationModelConfiguration, setOrganizationModelConfiguration] = useState<OrganizationAiModelConfigurationResponse | null>(null);
const [modelConfigurationLoading, setModelConfigurationLoading] = useState(true);
const [modelConfigurationError, setModelConfigurationError] = useState<string | null>(null);
const hasFetchedModelConfiguration = useRef(false);
const workflowId = workflow.id;
@ -1166,6 +1362,37 @@ function WorkflowSettingsInner({
user,
});
useEffect(() => {
if (hasFetchedModelConfiguration.current) return;
hasFetchedModelConfiguration.current = true;
const loadModelConfiguration = async () => {
setModelConfigurationLoading(true);
setModelConfigurationError(null);
const [defaultsResult, configurationResult] = await Promise.all([
getModelConfigurationV2DefaultsApiV1OrganizationsModelConfigurationsV2DefaultsGet(),
getModelConfigurationV2ApiV1OrganizationsModelConfigurationsV2Get(),
]);
if (defaultsResult.error) {
setModelConfigurationError(detailFromError(defaultsResult.error, "Failed to load model configuration defaults"));
setModelConfigurationLoading(false);
return;
}
if (configurationResult.error) {
setModelConfigurationError(detailFromError(configurationResult.error, "Failed to load model configuration"));
setModelConfigurationLoading(false);
return;
}
setModelConfigurationDefaults(defaultsResult.data as ModelConfigurationDefaultsV2);
setOrganizationModelConfiguration(configurationResult.data || null);
setModelConfigurationLoading(false);
};
loadModelConfiguration();
}, []);
// Intersection observer for active sidebar link
useEffect(() => {
const ids = NAV_ITEMS.map((n) => n.id);
@ -1218,37 +1445,15 @@ function WorkflowSettingsInner({
onSave={saveWorkflowConfigurations}
/>
{/* Model Overrides */}
<Card id="models">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<Brain className="h-4 w-4" />
Model Overrides
</CardTitle>
<CardDescription>
Override global model settings for this workflow. Toggle individual services to
customize.{" "}
<a href={SETTINGS_DOCUMENTATION_URLS.modelOverrides} target="_blank" rel="noopener noreferrer" className="inline-flex items-center gap-0.5 underline">Learn more <ExternalLink className="h-3 w-3" /></a>
</CardDescription>
</CardHeader>
<CardContent>
<ServiceConfigurationForm
mode="override"
currentOverrides={workflowConfigurations.model_overrides}
submitLabel="Save Model Overrides"
onSave={async (config) => {
await saveWorkflowConfigurations(
{
...workflowConfigurations,
model_overrides:
config.model_overrides as WorkflowConfigurations["model_overrides"],
} as WorkflowConfigurations,
workflowName,
);
}}
/>
</CardContent>
</Card>
<WorkflowModelOverridesSection
workflowConfigurations={workflowConfigurations}
workflowName={workflowName}
onSave={saveWorkflowConfigurations}
modelConfigurationDefaults={modelConfigurationDefaults}
organizationModelConfiguration={organizationModelConfiguration}
modelConfigurationLoading={modelConfigurationLoading}
modelConfigurationError={modelConfigurationError}
/>
{/* Template Variables */}
<TemplateVariablesSection