Skip to content

Commit 18d803a

Browse files
committed
feat(chat): expose model, reasoning effort and Astra Fast controls
1 parent 10878e2 commit 18d803a

9 files changed

Lines changed: 190 additions & 23 deletions

File tree

‎apps/sim/app/workspace/[workspaceId]/home/components/user-input/user-input.tsx‎

Lines changed: 98 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,19 @@ import {
1010
useRef,
1111
useState,
1212
} from 'react'
13-
import { Button, Chip, ChipDropdown, cn, Tooltip, toast } from '@sim/emcn'
14-
import { Paperclip, Plus, Slash } from '@sim/emcn/icons'
13+
import {
14+
Button,
15+
Chip,
16+
ChipDropdown,
17+
cn,
18+
Popover,
19+
PopoverContent,
20+
PopoverTrigger,
21+
Slider,
22+
Tooltip,
23+
toast,
24+
} from '@sim/emcn'
25+
import { ChevronDown, Paperclip, Plus, Slash, Zap } from '@sim/emcn/icons'
1526
import { createLogger } from '@sim/logger'
1627
import { useParams } from 'next/navigation'
1728
import { getMothershipAttachmentPreviewUrl } from '@/lib/mothership/chat/attachment-preview'
@@ -44,7 +55,7 @@ import { useVoiceInput } from '@/hooks/use-voice-input'
4455
import { type DraftPayload, useMothershipDraftsStore } from '@/stores/mothership-drafts/store'
4556
import {
4657
MOTHERSHIP_EFFORT_OPTIONS,
47-
type MothershipEffort,
58+
MOTHERSHIP_MODEL_OPTIONS,
4859
useMothershipEffortStore,
4960
} from '@/stores/mothership-effort/store'
5061
import type { ChatContext } from '@/stores/panel'
@@ -540,6 +551,9 @@ const UserInputImpl = forwardRef<UserInputHandle, UserInputProps>(function UserI
540551
editorRef.current.openResourceMenu({ left: rect.left, top: rect.top })
541552
}, [])
542553

554+
const modelSelection = useMothershipEffortStore((state) => state.modelSelection)
555+
const setModel = useMothershipEffortStore((state) => state.setModel)
556+
const setFastMode = useMothershipEffortStore((state) => state.setFastMode)
543557
const effort = useMothershipEffortStore((state) => state.effort)
544558
const setEffort = useMothershipEffortStore((state) => state.setEffort)
545559

@@ -618,16 +632,87 @@ const UserInputImpl = forwardRef<UserInputHandle, UserInputProps>(function UserI
618632
</Tooltip.Trigger>
619633
<Tooltip.Content side='top'>Skills</Tooltip.Content>
620634
</Tooltip.Root>
621-
<ChipDropdown
622-
variant='ghost'
623-
options={MOTHERSHIP_EFFORT_OPTIONS}
624-
value={effort}
625-
placeholder='Effort'
626-
aria-label='Effort'
627-
align='start'
628-
matchTriggerWidth={false}
629-
onChange={(value) => setEffort(value as MothershipEffort)}
630-
/>
635+
<Popover>
636+
<PopoverTrigger asChild>
637+
<Button
638+
type='button'
639+
variant='ghost'
640+
className='gap-1.5'
641+
aria-label='Model and reasoning effort'
642+
>
643+
{modelSelection.fastMode && <Zap className='size-[14px]' />}
644+
{
645+
MOTHERSHIP_MODEL_OPTIONS.find((option) => option.value === modelSelection.model)
646+
?.label
647+
}
648+
<span className='text-[var(--text-muted)]'>
649+
{MOTHERSHIP_EFFORT_OPTIONS.find((option) => option.value === effort)?.label}
650+
</span>
651+
<ChevronDown className='size-[14px]' />
652+
</Button>
653+
</PopoverTrigger>
654+
<PopoverContent side='top' align='start' minWidth={280} maxWidth={280}>
655+
<div className='flex flex-col gap-4 p-2'>
656+
<div className='flex items-center gap-2'>
657+
<ChipDropdown
658+
options={MOTHERSHIP_MODEL_OPTIONS}
659+
value={modelSelection.model}
660+
aria-label='Model'
661+
className='min-w-0 flex-1'
662+
onChange={(value) => {
663+
const option = MOTHERSHIP_MODEL_OPTIONS.find(
664+
(option) => option.value === value
665+
)
666+
if (option) setModel(option.value)
667+
}}
668+
/>
669+
{modelSelection.model === 'gpt-6-astra' && (
670+
<Tooltip.Root>
671+
<Tooltip.Trigger asChild>
672+
<Button
673+
type='button'
674+
variant={modelSelection.fastMode ? 'active' : 'ghost'}
675+
aria-label='Fast mode'
676+
aria-pressed={modelSelection.fastMode}
677+
onClick={() => setFastMode(!modelSelection.fastMode)}
678+
>
679+
<Zap className='size-[14px]' />
680+
</Button>
681+
</Tooltip.Trigger>
682+
<Tooltip.Content>
683+
Fast mode · faster responses at 2× the token cost
684+
</Tooltip.Content>
685+
</Tooltip.Root>
686+
)}
687+
</div>
688+
<div className='flex items-center justify-between text-small'>
689+
<span className='text-[var(--text-muted)]'>Reasoning effort</span>
690+
<span>
691+
{MOTHERSHIP_EFFORT_OPTIONS.find((option) => option.value === effort)?.label}
692+
</span>
693+
</div>
694+
<Slider
695+
aria-label='Reasoning effort'
696+
aria-valuetext={
697+
MOTHERSHIP_EFFORT_OPTIONS.find((option) => option.value === effort)?.label
698+
}
699+
min={0}
700+
max={MOTHERSHIP_EFFORT_OPTIONS.length - 1}
701+
step={1}
702+
value={[MOTHERSHIP_EFFORT_OPTIONS.findIndex((option) => option.value === effort)]}
703+
onValueChange={([index]) => {
704+
const option = MOTHERSHIP_EFFORT_OPTIONS[index]
705+
if (option) setEffort(option.value)
706+
}}
707+
/>
708+
<div className='flex justify-between text-[var(--text-muted)] text-caption'>
709+
{MOTHERSHIP_EFFORT_OPTIONS.map((option) => (
710+
<span key={option.value}>{option.label}</span>
711+
))}
712+
</div>
713+
</div>
714+
</PopoverContent>
715+
</Popover>
631716
</div>
632717
<div className='flex items-center gap-1.5'>
633718
{isSttSupported && (

‎apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3438,6 +3438,7 @@ export function useChat(
34383438
...desktopChatCapabilities,
34393439
userTimezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
34403440
effort: useMothershipEffortStore.getState().effort,
3441+
modelSelection: useMothershipEffortStore.getState().modelSelection,
34413442
}),
34423443
signal: abortController.signal,
34433444
})

‎apps/sim/lib/mothership/chat/payload.test.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,8 @@ describe('buildCopilotRequestPayload', () => {
529529
model: 'claude-opus-4-8',
530530
workspaceId: 'ws-1',
531531
userTimezone: 'America/Los_Angeles',
532+
effort: 'max',
533+
modelSelection: { model: 'gpt-6-astra', fastMode: true },
532534
},
533535
{ selectedModel: 'claude-opus-4-8' }
534536
)
@@ -540,6 +542,8 @@ describe('buildCopilotRequestPayload', () => {
540542
messageId: 'msg-1',
541543
workspaceId: 'ws-1',
542544
userTimezone: 'America/Los_Angeles',
545+
effort: 'max',
546+
modelSelection: { model: 'gpt-6-astra', fastMode: true },
543547
})
544548
)
545549
// Model/provider/mode are server-decided (P12); permissions are enforced by v2 under

‎apps/sim/lib/mothership/chat/payload.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { isHosted, isDocSandboxEnabled } from '@/lib/core/config/env-flags'
2222
import { isOAuthServiceDeploymentAvailable } from '@/lib/integrations/availability.server'
2323
import { buildUploadedFileContext } from '@/lib/mothership/chat/upload-context'
2424
import { buildWorkspaceInventory } from '@/lib/mothership/chat/workspace-inventory'
25-
import type { ChatRequest } from '@/lib/mothership/generated/protocol'
25+
import type { ChatRequest, ModelSelection } from '@/lib/mothership/generated/protocol'
2626
import {
2727
type IntegrationGateConfig,
2828
integrationGateSignature,
@@ -75,6 +75,7 @@ interface BuildPayloadParams {
7575
userTimezone?: string
7676
/** Per-turn model effort dial (user-selected in the composer). */
7777
effort?: 'low' | 'medium' | 'high' | 'xhigh' | 'max'
78+
modelSelection?: ModelSelection
7879
desktopLocalFilesystem?: boolean
7980
browser?: boolean
8081
terminalCapable?: boolean
@@ -373,7 +374,7 @@ export async function buildCopilotRequestPayload(
373374
}
374375

375376
// The wire payload IS the shared contract (ChatRequest in lib/mothership/generated/
376-
// protocol.ts) — nothing else. Model/provider/mode are server-decided (P12); permissions
377+
// protocol.ts) — nothing else. The closed model selection is separate from raw model/provider/mode; permissions
377378
// are enforced by v2 under the delegation token, not asserted here; desktop capabilities
378379
// are out of scope for v1. The params above still carry sim-internal knowledge (mode
379380
// gates which tool schemas get built), but none of it rides the wire.
@@ -404,6 +405,7 @@ export async function buildCopilotRequestPayload(
404405
...(mothershipTools.length > 0 ? { mothershipTools } : {}),
405406
...(params.userTimezone ? { userTimezone: params.userTimezone } : {}),
406407
...(params.effort ? { effort: params.effort } : {}),
408+
...(params.modelSelection ? { modelSelection: params.modelSelection } : {}),
407409
...(inventory ? { inventory } : {}),
408410
// The mounted chat view executes client-routed workflow tools (run panel UX), so the
409411
// UI declares that capability explicitly; headless callers omit or send [] and the

‎apps/sim/lib/mothership/chat/post.test.ts‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -718,6 +718,8 @@ describe('handleUnifiedChatPost', () => {
718718
method: 'POST',
719719
body: JSON.stringify({
720720
message: 'Hello',
721+
effort: 'xhigh',
722+
modelSelection: { model: 'gpt-6-astra', fastMode: true },
721723
workflowId: 'wf-1',
722724
workspaceId: 'ws-1',
723725
}),
@@ -730,6 +732,8 @@ describe('handleUnifiedChatPost', () => {
730732
expect(buildCopilotRequestPayload).toHaveBeenCalledWith(
731733
expect.objectContaining({
732734
message: 'Hello',
735+
effort: 'xhigh',
736+
modelSelection: { model: 'gpt-6-astra', fastMode: true },
733737
userId: 'user-1',
734738
workflowId: 'wf-1',
735739
workspaceId: 'ws-1',
@@ -787,6 +791,8 @@ describe('handleUnifiedChatPost', () => {
787791
method: 'POST',
788792
body: JSON.stringify({
789793
message: 'Hello',
794+
effort: 'xhigh',
795+
modelSelection: { model: 'gpt-6-astra', fastMode: true },
790796
workspaceId: 'ws-1',
791797
createNewChat: true,
792798
}),
@@ -797,6 +803,8 @@ describe('handleUnifiedChatPost', () => {
797803
expect(buildCopilotRequestPayload).toHaveBeenCalledWith(
798804
expect.objectContaining({
799805
message: 'Hello',
806+
effort: 'xhigh',
807+
modelSelection: { model: 'gpt-6-astra', fastMode: true },
800808
userId: 'user-1',
801809
workspaceId: 'ws-1',
802810
}),
@@ -1664,14 +1672,14 @@ describe('handleUnifiedChatPost copilot.use capability gate', () => {
16641672
beforeEach(() => {
16651673
vi.clearAllMocks()
16661674
resetDbChainMock()
1667-
getSession.mockResolvedValue({ user: { id: 'user-1' } })
1675+
getSession.mockResolvedValue({ user: { id: 'user-1' }, session: { id: 'session-1' } })
16681676
atomicallyClaimChatSend.mockResolvedValue({
16691677
claimed: true,
16701678
normalizedKey: 'chat-send:user-message:msg-1:userId=user-1',
16711679
storageMethod: 'database',
16721680
claimToken: 'claim-1',
16731681
})
1674-
storeChatSendResult.mockResolvedValue(true)
1682+
admitTurn.mockResolvedValue({ id: 'run-1', status: 'active' })
16751683
releaseChatSendClaim.mockResolvedValue(undefined)
16761684
resolveWorkflowIdForUser.mockResolvedValue({
16771685
status: 'resolved',
@@ -1698,7 +1706,6 @@ describe('handleUnifiedChatPost copilot.use capability gate', () => {
16981706
conflicts: [],
16991707
decryptionFailures: [],
17001708
})
1701-
generateWorkspaceSnapshot.mockResolvedValue({ markdown: '', snapshot: { workflows: [] } })
17021709
processContextsServer.mockResolvedValue([])
17031710
resolveActiveResourceContext.mockResolvedValue(null)
17041711
buildCopilotRequestPayload.mockImplementation(async (params: Record<string, unknown>) => params)

‎apps/sim/lib/mothership/chat/post.ts‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,12 @@ import {
4646
} from '@/lib/mothership/chat/selection-context'
4747
import { COPILOT_REQUEST_MODES } from '@/lib/mothership/constants'
4848
import { prepareCopilotEnvironmentContext } from '@/lib/mothership/environment-context'
49-
import { type ChatRequest, PROTOCOL_VERSION } from '@/lib/mothership/generated/protocol'
49+
import {
50+
type ChatRequest,
51+
type ModelSelection,
52+
ModelSelectionSchema,
53+
PROTOCOL_VERSION,
54+
} from '@/lib/mothership/generated/protocol'
5055
import { CopilotTransport } from '@/lib/mothership/generated/trace-attribute-values-v1'
5156
import { TraceAttr } from '@/lib/mothership/generated/trace-attributes-v1'
5257
import { TraceSpan } from '@/lib/mothership/generated/trace-spans-v1'
@@ -268,6 +273,7 @@ const ChatMessageSchema = z
268273
commands: z.array(z.string()).optional(),
269274
userTimezone: z.string().optional(),
270275
effort: z.enum(['low', 'medium', 'high', 'xhigh', 'max']).optional(),
276+
modelSelection: ModelSelectionSchema.optional(),
271277
clientCapabilities: z.array(z.string()).optional(),
272278
desktopCapabilities: z
273279
.object({
@@ -338,6 +344,7 @@ type UnifiedChatBranch =
338344
userPermission?: string
339345
userTimezone?: string
340346
effort?: 'low' | 'medium' | 'high' | 'xhigh' | 'max'
347+
modelSelection?: ModelSelection
341348
workflowId: string
342349
workflowName?: string
343350
workspaceId: string
@@ -388,6 +395,7 @@ type UnifiedChatBranch =
388395
workspaceContext?: string
389396
vfs?: VfsSnapshotV1
390397
effort?: 'low' | 'medium' | 'high' | 'xhigh' | 'max'
398+
modelSelection?: ModelSelection
391399
desktopLocalFilesystem?: boolean
392400
browser?: boolean
393401
terminalCapable?: boolean
@@ -709,6 +717,7 @@ async function resolveBranch(params: {
709717
userPermission: payloadParams.userPermission,
710718
userTimezone: payloadParams.userTimezone,
711719
effort: payloadParams.effort,
720+
modelSelection: payloadParams.modelSelection,
712721
desktopLocalFilesystem: payloadParams.desktopLocalFilesystem,
713722
browser: payloadParams.browser,
714723
terminalCapable: payloadParams.terminalCapable,
@@ -770,6 +779,7 @@ async function resolveBranch(params: {
770779
userPermission: payloadParams.userPermission,
771780
userTimezone: payloadParams.userTimezone,
772781
effort: payloadParams.effort,
782+
modelSelection: payloadParams.modelSelection,
773783
desktopLocalFilesystem: payloadParams.desktopLocalFilesystem,
774784
browser: payloadParams.browser,
775785
terminalCapable: payloadParams.terminalCapable,
@@ -1233,6 +1243,7 @@ export async function handleUnifiedChatPost(req: NextRequest) {
12331243
userPermission: userPermission ?? undefined,
12341244
userTimezone: body.userTimezone,
12351245
effort: body.effort,
1246+
modelSelection: body.modelSelection,
12361247
workflowId: branch.workflowId,
12371248
workflowName: branch.workflowName,
12381249
workspaceId: branch.workspaceId,
@@ -1260,6 +1271,7 @@ export async function handleUnifiedChatPost(req: NextRequest) {
12601271
userPermission: userPermission ?? undefined,
12611272
userTimezone: body.userTimezone,
12621273
effort: body.effort,
1274+
modelSelection: body.modelSelection,
12631275
desktopLocalFilesystem: body.desktopCapabilities?.localFilesystem === true,
12641276
browser: body.desktopCapabilities?.browser === true,
12651277
terminalCapable: body.desktopCapabilities?.terminal === true,

‎apps/sim/lib/mothership/generated/protocol.ts‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,17 @@ const WorkspaceInventorySchema = z.object({
6767
truncated: z.array(z.string()),
6868
});
6969

70+
/** Composer choices are a closed catalog; credentials and provider routes stay server-owned. */
71+
export const ModelSelectionSchema = z
72+
.object({
73+
model: z.enum(["gpt-6-astra", "claude-opus-5"]),
74+
fastMode: z.boolean().default(false),
75+
})
76+
.refine((selection) => !selection.fastMode || selection.model === "gpt-6-astra", {
77+
message: "Fast mode is available only for GPT-6 Astra",
78+
});
79+
export type ModelSelection = z.infer<typeof ModelSelectionSchema>;
80+
7081
export const ChatPayloadSchema = z.strictObject({
7182
simConnection: SimConnection.optional(),
7283
message: z.string().min(1),
@@ -109,6 +120,7 @@ export const ChatPayloadSchema = z.strictObject({
109120
origin: z.enum(["task"]).optional(),
110121
/** Per-turn effort dial (user-selected in the composer); absent = deployment default. */
111122
effort: z.enum(["low", "medium", "high", "xhigh", "max"]).optional(),
123+
modelSelection: ModelSelectionSchema.optional(),
112124
/** Workspace orientation (contracts ChatRequest.inventory): names and ids per world. */
113125
inventory: WorkspaceInventorySchema.optional(),
114126
});
@@ -135,6 +147,8 @@ export interface StreamToolReplay {
135147

136148
/** POST /api/mothership — the chat request sim sends. */
137149
export interface ChatRequest extends StreamResponseReceipt {
150+
effort?: "low" | "medium" | "high" | "xhigh" | "max" | undefined;
151+
modelSelection?: ModelSelection | undefined;
138152
simConnection?: SimConnection | undefined;
139153
message: string;
140154
userId: string;

0 commit comments

Comments
 (0)