diff --git a/packages/ui/src/features/sessions/hooks/useAvailableModelOptions.ts b/packages/ui/src/features/sessions/hooks/useAvailableModelOptions.ts
new file mode 100644
index 0000000000..e1d0f9f231
--- /dev/null
+++ b/packages/ui/src/features/sessions/hooks/useAvailableModelOptions.ts
@@ -0,0 +1,37 @@
+import { useHostTRPC } from "@posthog/host-router/react";
+import { flattenSelectOptions, getCloudUrlFromRegion } from "@posthog/shared";
+import { useAuthStateValue } from "@posthog/ui/features/auth/store";
+import { useQuery } from "@tanstack/react-query";
+import { useMemo } from "react";
+
+export interface ModelOption {
+ value: string;
+ name: string;
+}
+
+export function useAvailableModelOptions(
+ adapter: "claude" | "codex",
+): ModelOption[] {
+ const hostTRPC = useHostTRPC();
+ const cloudRegion = useAuthStateValue((state) => state.cloudRegion);
+ const apiHost = useMemo(
+ () => (cloudRegion ? getCloudUrlFromRegion(cloudRegion) : null),
+ [cloudRegion],
+ );
+
+ const { data: configOptions } = useQuery({
+ ...hostTRPC.agent.getPreviewConfigOptions.queryOptions({
+ apiHost: apiHost ?? "",
+ adapter,
+ }),
+ enabled: !!apiHost,
+ });
+
+ return useMemo(() => {
+ const modelOpt = configOptions?.find(
+ (o) => o.category === "model" || o.id === "model",
+ );
+ if (!modelOpt || modelOpt.type !== "select") return [];
+ return flattenSelectOptions(modelOpt.options);
+ }, [configOptions]);
+}
diff --git a/packages/ui/src/features/settings/sections/GeneralSettings.tsx b/packages/ui/src/features/settings/sections/GeneralSettings.tsx
index e1563633c4..89f0a59210 100644
--- a/packages/ui/src/features/settings/sections/GeneralSettings.tsx
+++ b/packages/ui/src/features/settings/sections/GeneralSettings.tsx
@@ -7,11 +7,13 @@ import {
COLLAPSE_MODE_OPTIONS,
type CollapseMode,
} from "@posthog/ui/features/sessions/components/new-thread/conversationThreadConfig";
+import { useAvailableModelOptions } from "@posthog/ui/features/sessions/hooks/useAvailableModelOptions";
import { SettingRow } from "@posthog/ui/features/settings/SettingRow";
import {
type AutoConvertLongText,
type DefaultInitialTaskMode,
type DefaultMessagingMode,
+ type DefaultModel,
type DefaultReasoningEffort,
type DiffOpenMode,
type SendMessagesWith,
@@ -71,6 +73,7 @@ export function GeneralSettings() {
autoConvertLongText,
defaultInitialTaskMode,
defaultMessagingMode,
+ defaultModels,
defaultReasoningEffort,
diffOpenMode,
sendMessagesWith,
@@ -81,6 +84,7 @@ export function GeneralSettings() {
setAutoConvertLongText,
setDefaultInitialTaskMode,
setDefaultMessagingMode,
+ setDefaultModel,
setDefaultReasoningEffort,
setDiffOpenMode,
setSendMessagesWith,
@@ -90,6 +94,9 @@ export function GeneralSettings() {
setBrainrotMode,
} = useSettingsStore();
+ const claudeModelOptions = useAvailableModelOptions("claude");
+ const codexModelOptions = useAvailableModelOptions("codex");
+
// Appearance handlers
const handleThemeChange = useCallback(
(value: ThemePreference) => {
@@ -164,6 +171,18 @@ export function GeneralSettings() {
[defaultReasoningEffort, setDefaultReasoningEffort],
);
+ const handleDefaultModelChange = useCallback(
+ (adapter: "claude" | "codex", value: DefaultModel) => {
+ track(ANALYTICS_EVENTS.SETTING_CHANGED, {
+ setting_name: "default_model",
+ new_value: value,
+ old_value: defaultModels[adapter] ?? "last_used",
+ });
+ setDefaultModel(adapter, value);
+ },
+ [defaultModels, setDefaultModel],
+ );
+
const handleConversationCollapseModeChange = useCallback(
(value: CollapseMode) => {
track(ANALYTICS_EVENTS.SETTING_CHANGED, {
@@ -311,6 +330,55 @@ export function GeneralSettings() {
+
+
+ handleDefaultModelChange("claude", value as DefaultModel)
+ }
+ size="1"
+ disabled={claudeModelOptions.length === 0}
+ >
+
+
+ Last used
+ {claudeModelOptions.map((opt) => (
+
+ {opt.name}
+
+ ))}
+
+
+
+
+ {codexModelOptions.length > 0 && (
+
+
+ handleDefaultModelChange("codex", value as DefaultModel)
+ }
+ size="1"
+ >
+
+
+ Last used
+ {codexModelOptions.map((opt) => (
+
+ {opt.name}
+
+ ))}
+
+
+
+ )}
+
>;
defaultMessagingMode: DefaultMessagingMode;
setDefaultMessagingMode: (mode: DefaultMessagingMode) => void;
setDefaultRunMode: (mode: DefaultRunMode) => void;
@@ -113,6 +116,7 @@ interface SettingsStore {
setLastUsedInitialTaskMode: (mode: ExecutionMode) => void;
setLastPlanApprovalMode: (mode: ExecutionMode) => void;
setDefaultReasoningEffort: (effort: DefaultReasoningEffort) => void;
+ setDefaultModel: (adapter: "claude" | "codex", model: DefaultModel) => void;
// Notifications
desktopNotifications: boolean;
@@ -228,6 +232,7 @@ export const useSettingsStore = create()(
lastUsedInitialTaskMode: "plan",
lastPlanApprovalMode: null,
defaultReasoningEffort: "last_used",
+ defaultModels: {},
defaultMessagingMode: "queue",
setDefaultRunMode: (mode) => set({ defaultRunMode: mode }),
setLastUsedRunMode: (mode) => set({ lastUsedRunMode: mode }),
@@ -261,6 +266,10 @@ export const useSettingsStore = create()(
setLastPlanApprovalMode: (mode) => set({ lastPlanApprovalMode: mode }),
setDefaultReasoningEffort: (effort) =>
set({ defaultReasoningEffort: effort }),
+ setDefaultModel: (adapter, model) =>
+ set((state) => ({
+ defaultModels: { ...state.defaultModels, [adapter]: model },
+ })),
setDefaultMessagingMode: (mode) => set({ defaultMessagingMode: mode }),
// Notifications
diff --git a/packages/ui/src/features/task-detail/hooks/usePreviewConfig.ts b/packages/ui/src/features/task-detail/hooks/usePreviewConfig.ts
index ccde175ece..2f82c7e0fb 100644
--- a/packages/ui/src/features/task-detail/hooks/usePreviewConfig.ts
+++ b/packages/ui/src/features/task-detail/hooks/usePreviewConfig.ts
@@ -89,8 +89,10 @@ export function usePreviewConfig(
lastUsedInitialTaskMode,
defaultReasoningEffort,
lastUsedReasoningEffort,
+ defaultModels,
lastUsedModel,
} = useSettingsStore.getState();
+ const defaultModel = defaultModels[adapter] ?? "last_used";
let initial = deriveInitialConfig(
options,
@@ -107,19 +109,22 @@ export function usePreviewConfig(
// without this the user's last pick (e.g. fable) is lost on every
// refetch/remount. Restore it through applyConfigChange so the dependent
// effort options are recomputed for the restored model.
+ // If defaultModel is set (not "last_used"), prefer it over lastUsedModel.
+ const targetModel =
+ defaultModel !== "last_used" ? defaultModel : lastUsedModel;
const modelOpt = getOptionByCategory(initial, "model");
if (
- lastUsedModel &&
+ targetModel &&
modelOpt?.type === "select" &&
- modelOpt.currentValue !== lastUsedModel &&
- flattenConfigValues(modelOpt).includes(lastUsedModel)
+ modelOpt.currentValue !== targetModel &&
+ flattenConfigValues(modelOpt).includes(targetModel)
) {
initial = applyConfigChange(initial, {
adapter,
configId: modelOpt.id,
- value: lastUsedModel,
+ value: targetModel,
effortOptions:
- getReasoningEffortOptions(adapter, lastUsedModel) ?? undefined,
+ getReasoningEffortOptions(adapter, targetModel) ?? undefined,
settings: {
defaultInitialTaskMode: "",
lastUsedInitialTaskMode: undefined,