From e3215701ef7c5740326ed0cb4ddd53a1a2bd9a50 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Mon, 5 Oct 2026 23:22:45 -0700 Subject: [PATCH 1/3] feat(mothership): let the worker choose models when the model picker is off With the picker off, chats sent the hidden hosted default selection (GPT-6 Astra), which the worker treats as an explicit pick for both the main agent and subagents. No selection is sent now, so the worker routes every model role itself. A stale client selection is still cleared at admission. Picker-on chats and the Sim Chat block are unchanged. --- .../user-input/components/model-selector.tsx | 12 ++++----- apps/sim/lib/mothership/chat/post.test.ts | 12 ++++----- apps/sim/lib/mothership/model-options.ts | 25 ++++++++++++------- apps/sim/stores/mothership-effort/store.ts | 7 ++---- 4 files changed, 30 insertions(+), 26 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/model-selector.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/model-selector.tsx index 871225d7ea5..83a7f75c1f1 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/model-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/model-selector.tsx @@ -45,7 +45,7 @@ export function ModelSelector() { { effort: effortChoice ?? undefined, modelSelection: selection }, advanced ) - const options = advanced + const options = modelSelection ? mothershipEffortOptions(modelSelection.model) : MOTHERSHIP_SIMPLE_EFFORT_OPTIONS const setEffort = (choice: MothershipEffort) => { @@ -59,13 +59,10 @@ export function ModelSelector() { }, [chatId]) const effortLabel = options.find((option) => option.value === effort)?.label ?? effort - const modelLabel = - MOTHERSHIP_MODEL_OPTIONS.find((option) => option.value === modelSelection.model)?.label ?? - modelSelection.model return (
- {advanced && ( + {modelSelection && ( <> {modelSelection.model !== 'claude-opus-5-5' && ( option.value === modelSelection.model) + ?.label ?? modelSelection.model + } icon={Sparkles} showChevron /> diff --git a/apps/sim/lib/mothership/chat/post.test.ts b/apps/sim/lib/mothership/chat/post.test.ts index 86dcab8c45f..a7d3c38eec3 100644 --- a/apps/sim/lib/mothership/chat/post.test.ts +++ b/apps/sim/lib/mothership/chat/post.test.ts @@ -816,7 +816,7 @@ describe('handleUnifiedChatPost', () => { ['low', 'low'], ['none', 'high'], ])( - 'enforces the default model and effort range on submitted %s effort', + 'leaves the model to the worker and enforces the effort range on submitted %s effort', async (effort, expected) => { flags.models.mockResolvedValue(false) const response = await handleUnifiedChatPost( @@ -831,11 +831,11 @@ describe('handleUnifiedChatPost', () => { }) ) expect(response.status).toBe(200) - expect(buildCopilotRequestPayload).toHaveBeenCalledWith( - expect.objectContaining({ - effort: expected, - modelSelection: { model: 'gpt-6-astra', fastMode: false }, - }) + const params = buildCopilotRequestPayload.mock.lastCall?.[0] as Record + expect(params?.effort).toBe(expected) + expect(params?.modelSelection).toBeUndefined() + expect(startCopilotOtelRoot.mock.results.at(-1)?.value.setRequestShape).toHaveBeenCalledWith( + expect.objectContaining({ model: undefined }) ) } ) diff --git a/apps/sim/lib/mothership/model-options.ts b/apps/sim/lib/mothership/model-options.ts index b4d26c75570..aff834b7ab1 100644 --- a/apps/sim/lib/mothership/model-options.ts +++ b/apps/sim/lib/mothership/model-options.ts @@ -38,22 +38,29 @@ export function mothershipEffortOptions(model: ModelSelection['model']) { : MOTHERSHIP_EFFORT_OPTIONS } -/** Shared by the visible controls, send path and server admission so hidden preferences cannot leak. */ +/** + * Shared by the visible controls, send path and server admission so hidden preferences cannot leak. + * Without the model picker no selection is sent, so the worker routes every model role itself. + */ export function resolveMothershipModelSettings( settings: { effort?: MothershipEffort; modelSelection?: ModelSelection }, advanced: boolean -): { effort: MothershipEffort; modelSelection: ModelSelection } { +): { effort: MothershipEffort; modelSelection: ModelSelection | undefined } { let effort = settings.effort ?? DEFAULT_MOTHERSHIP_EFFORT if (!advanced) { if (effort === 'none') effort = DEFAULT_MOTHERSHIP_EFFORT if (effort === 'max') effort = 'xhigh' - return { effort, modelSelection: { model: 'gpt-6-astra', fastMode: false } } + return { effort, modelSelection: undefined } } - const stored = settings.modelSelection ?? { model: 'gpt-6-astra', fastMode: false } + const modelSelection = normalizeModelSelection( + settings.modelSelection ?? { model: 'gpt-6-astra', fastMode: false } + ) + if (effort === 'none' && modelSelection.model !== 'gpt-6-sol') effort = DEFAULT_MOTHERSHIP_EFFORT + return { effort, modelSelection } +} + +/** A stored pick on the current catalog: Opus 5 reads as Opus 5.5, which has no Fast mode. */ +export function normalizeModelSelection(stored: ModelSelection): ModelSelection { const model = stored.model === 'claude-opus-5' ? 'claude-opus-5-5' : stored.model - if (effort === 'none' && model !== 'gpt-6-sol') effort = DEFAULT_MOTHERSHIP_EFFORT - return { - effort, - modelSelection: { model, fastMode: model === 'claude-opus-5-5' ? false : stored.fastMode }, - } + return { model, fastMode: model === 'claude-opus-5-5' ? false : stored.fastMode } } diff --git a/apps/sim/stores/mothership-effort/store.ts b/apps/sim/stores/mothership-effort/store.ts index a885b641931..0ec4b53977b 100644 --- a/apps/sim/stores/mothership-effort/store.ts +++ b/apps/sim/stores/mothership-effort/store.ts @@ -2,10 +2,7 @@ import { omit, toRecord } from '@sim/utils/object' import { create } from 'zustand' import { devtools, persist } from 'zustand/middleware' import { type ModelSelection, ModelSelectionSchema } from '@/lib/mothership/generated/protocol' -import { - type MothershipEffort, - resolveMothershipModelSettings, -} from '@/lib/mothership/model-options' +import { type MothershipEffort, normalizeModelSelection } from '@/lib/mothership/model-options' /** A chat's effort pick and the token that tells it apart from other picks of the same value. */ interface ChatEffortPick { @@ -52,7 +49,7 @@ let lastChatEffortPick = 0 function withModelSelection( modelSelection: ModelSelection ): Pick { - return { modelSelection: resolveMothershipModelSettings({ modelSelection }, true).modelSelection } + return { modelSelection: normalizeModelSelection(modelSelection) } } export const useMothershipEffortStore = create()( From 1d1195c1eb140d43028e107e11e84d38d87a316f Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Mon, 5 Oct 2026 23:32:16 -0700 Subject: [PATCH 2/3] test(mothership): assert the omitted selection on the serialized worker request --- apps/sim/lib/mothership/chat/post.test.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/apps/sim/lib/mothership/chat/post.test.ts b/apps/sim/lib/mothership/chat/post.test.ts index a7d3c38eec3..cfce99968fc 100644 --- a/apps/sim/lib/mothership/chat/post.test.ts +++ b/apps/sim/lib/mothership/chat/post.test.ts @@ -831,12 +831,10 @@ describe('handleUnifiedChatPost', () => { }) ) expect(response.status).toBe(200) - const params = buildCopilotRequestPayload.mock.lastCall?.[0] as Record - expect(params?.effort).toBe(expected) - expect(params?.modelSelection).toBeUndefined() - expect(startCopilotOtelRoot.mock.results.at(-1)?.value.setRequestShape).toHaveBeenCalledWith( - expect.objectContaining({ model: undefined }) - ) + const { requestPayload } = createSSEStream.mock.lastCall?.[0] ?? {} + const wire = JSON.parse(JSON.stringify(requestPayload)) + expect(wire.effort).toBe(expected) + expect(wire).not.toHaveProperty('modelSelection') } ) From d5c53d19cf61e6cacc18f86d3b764c7978ee2b7d Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Mon, 5 Oct 2026 23:43:45 -0700 Subject: [PATCH 3/3] test(mothership): read the serialized request without a JSON round trip --- apps/sim/lib/mothership/chat/post.test.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/sim/lib/mothership/chat/post.test.ts b/apps/sim/lib/mothership/chat/post.test.ts index cfce99968fc..9e804f6fca8 100644 --- a/apps/sim/lib/mothership/chat/post.test.ts +++ b/apps/sim/lib/mothership/chat/post.test.ts @@ -832,9 +832,8 @@ describe('handleUnifiedChatPost', () => { ) expect(response.status).toBe(200) const { requestPayload } = createSSEStream.mock.lastCall?.[0] ?? {} - const wire = JSON.parse(JSON.stringify(requestPayload)) - expect(wire.effort).toBe(expected) - expect(wire).not.toHaveProperty('modelSelection') + expect(requestPayload?.effort).toBe(expected) + expect(JSON.stringify(requestPayload)).not.toContain('modelSelection') } )