From 9b17ce8f2cd769a2fcd761a1333a91c4dd33b3ec Mon Sep 17 00:00:00 2001 From: Saxon Fletcher Date: Wed, 2 Sep 2026 10:24:37 +1000 Subject: [PATCH] chore(studio): default assistant to GPT-5.6 Luna (#49749) ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Feature / chore: hide assistant model selection in the UI and default chats to GPT-5.6 Luna. ## What is the current behavior? The assistant composer exposes a model picker. Paid orgs default to `gpt-5.3-codex`; everyone else defaults to `gpt-5.4-nano`. ## What is the new behavior? - The model picker is hidden in the assistant composer and Explorer home. - Chats default to `gpt-5.6-luna` with `reasoningEffort: medium`. - Model selection plumbing is kept (registry, entitlements, `setModel`, generate-v4 request body) so a requested model can still be honored when provided. - Other completion endpoints still use `gpt-5.4-nano`. ## Additional context Model selector UI can be re-enabled by passing `selectedModel` / `onSelectModel` to `AssistantChatForm`. ## Summary by CodeRabbit * **New Features** * Added support for the GPT-5.6 Luna model with medium reasoning capability. * Made GPT-5.6 Luna the default assistant model. * **Improvements** * Simplified assistant chat by removing model selection from the primary chat experience. * Updated model fallback behavior to use the standard assistant model. * Chat forms can now optionally display model selection when configured. * **Tests** * Updated model coverage and assistant chat tests for the new defaults and behavior. --------- Co-authored-by: Cursor --- .../interfaces/Explorer/ExplorerHomeTab.tsx | 6 +--- .../Explorer/__tests__/hooks.test.tsx | 11 ++----- .../ui/AIAssistantPanel/AssistantChat.tsx | 32 +------------------ .../ui/AIAssistantPanel/AssistantChatForm.tsx | 20 ++++++++---- apps/studio/lib/ai/model.test.ts | 3 +- apps/studio/lib/ai/model.utils.test.ts | 23 +++++++------ apps/studio/lib/ai/model.utils.ts | 20 ++++++++---- apps/studio/pages/api/ai/sql/generate-v4.ts | 3 +- .../state/ai-assistant-state.hooks.test.tsx | 1 - apps/studio/state/ai-assistant-state.test.ts | 1 - 10 files changed, 46 insertions(+), 74 deletions(-) diff --git a/apps/studio/components/interfaces/Explorer/ExplorerHomeTab.tsx b/apps/studio/components/interfaces/Explorer/ExplorerHomeTab.tsx index 24272c3f941e3..e5a6158675382 100644 --- a/apps/studio/components/interfaces/Explorer/ExplorerHomeTab.tsx +++ b/apps/studio/components/interfaces/Explorer/ExplorerHomeTab.tsx @@ -5,7 +5,6 @@ import { useCreateChat, useCreateNotebook, useCreateQuery } from './hooks' import { CHAT_TEMPLATES, NOTEBOOK_TEMPLATES } from './templates' import { ActionCard } from '@/components/layouts/Tabs/ActionCard' import { AssistantChatForm } from '@/components/ui/AIAssistantPanel/AssistantChatForm' -import type { AssistantModel } from '@/state/ai-assistant-state' export const ExplorerHomeTab = () => { const { createNotebook } = useCreateNotebook() @@ -13,7 +12,6 @@ export const ExplorerHomeTab = () => { const { createChat } = useCreateChat() const [value, setValue] = useState('') - const [selectedModel, setSelectedModal] = useState('gpt-5.4-nano') return (
@@ -31,9 +29,7 @@ export const ExplorerHomeTab = () => { placeholder="Ask anything about your project" value={value} onValueChange={(e) => setValue(e.target.value)} - selectedModel={selectedModel} - onSelectModel={setSelectedModal} - onSubmit={(message) => createChat({ initialMessage: message, model: selectedModel })} + onSubmit={(message) => createChat({ initialMessage: message })} />
diff --git a/apps/studio/components/interfaces/Explorer/__tests__/hooks.test.tsx b/apps/studio/components/interfaces/Explorer/__tests__/hooks.test.tsx index 7b5e476aceb5c..8398181396a1d 100644 --- a/apps/studio/components/interfaces/Explorer/__tests__/hooks.test.tsx +++ b/apps/studio/components/interfaces/Explorer/__tests__/hooks.test.tsx @@ -9,7 +9,6 @@ const { mockPush, mockSelectChat, mockSetContext, - mockSetModel, mockWhenInitialized, } = vi.hoisted(() => ({ mockCreateChat: vi.fn(() => 'chat-2'), @@ -17,7 +16,6 @@ const { mockPush: vi.fn(), mockSelectChat: vi.fn(), mockSetContext: vi.fn(), - mockSetModel: vi.fn(), mockWhenInitialized: vi.fn(() => Promise.resolve()), })) @@ -41,7 +39,6 @@ vi.mock('@/state/ai-assistant-state', () => ({ createChat: mockCreateChat, selectChat: mockSelectChat, setContext: mockSetContext, - setModel: mockSetModel, }), whenAiAssistantInitialized: () => mockWhenInitialized(), })) @@ -59,7 +56,6 @@ describe('useCreateChat', () => { await result.current.createChat({ name: 'Investigate errors', initialMessage: 'What happened?', - model: 'gpt-5.4-nano', }) }) @@ -72,7 +68,6 @@ describe('useCreateChat', () => { name: 'Investigate errors', initialMessage: 'What happened?', }) - expect(mockSetModel).toHaveBeenCalledWith('gpt-5.4-nano') expect(mockPush).toHaveBeenCalledWith('/project/default/explorer/chat/chat-2') expect(mockSelectChat).not.toHaveBeenCalled() @@ -82,7 +77,7 @@ describe('useCreateChat', () => { expect(mockSelectChat).not.toHaveBeenCalled() }) - // Hydration replaces the chat map and the model wholesale, so a chat created mid-load would be + // Hydration replaces the chat map wholesale, so a chat created mid-load would be // dropped the moment the persisted state lands it('waits for the assistant state to hydrate before creating the chat', async () => { let resolveHydration = () => {} @@ -97,11 +92,10 @@ describe('useCreateChat', () => { let created: Promise | undefined await act(async () => { - created = result.current.createChat({ name: 'Investigate errors', model: 'gpt-5.4-nano' }) + created = result.current.createChat({ name: 'Investigate errors' }) }) expect(mockCreateChat).not.toHaveBeenCalled() - expect(mockSetModel).not.toHaveBeenCalled() expect(mockPush).not.toHaveBeenCalled() await act(async () => { @@ -113,7 +107,6 @@ describe('useCreateChat', () => { name: 'Investigate errors', initialMessage: undefined, }) - expect(mockSetModel).toHaveBeenCalledWith('gpt-5.4-nano') expect(mockPush).toHaveBeenCalledWith('/project/default/explorer/chat/chat-2') }) }) diff --git a/apps/studio/components/ui/AIAssistantPanel/AssistantChat.tsx b/apps/studio/components/ui/AIAssistantPanel/AssistantChat.tsx index 1a9aa92f849b3..20a618426843a 100644 --- a/apps/studio/components/ui/AIAssistantPanel/AssistantChat.tsx +++ b/apps/studio/components/ui/AIAssistantPanel/AssistantChat.tsx @@ -32,23 +32,16 @@ import { Markdown } from '@/components/interfaces/Markdown' import { useCheckOpenAIKeyQuery } from '@/data/ai/check-api-key-query' import { useRateMessageMutation } from '@/data/ai/rate-message-mutation' import { useTablesQuery } from '@/data/tables/tables-query' -import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements' import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage' import { useOrgAiOptInLevel } from '@/hooks/misc/useOrgOptedIntoAi' import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' import type { AssistantMessageMetadata } from '@/lib/ai/assistant-message-metadata' import { getParallelApprovalIdsToReject } from '@/lib/ai/message-utils' -import { - DEFAULT_ASSISTANT_BASE_MODEL_ID, - defaultAssistantModelId, - isAssistantBaseModelId, - isKnownAssistantModelId, -} from '@/lib/ai/model.utils' import { IS_PLATFORM } from '@/lib/constants' import { uuidv4 } from '@/lib/helpers' import { useTrack } from '@/lib/telemetry/track' -import type { AssistantModel, SqlSnippet } from '@/state/ai-assistant-state' +import type { SqlSnippet } from '@/state/ai-assistant-state' import { useAiAssistantState, useAiAssistantStateSnapshot } from '@/state/ai-assistant-state' import { SHORTCUT_IDS } from '@/state/shortcuts/registry' import { useShortcut } from '@/state/shortcuts/useShortcut' @@ -109,26 +102,6 @@ export const AssistantChat = ({ enabled: shortcutsEnabled, }) - const { hasAccess: hasAccessToAdvanceModel, isLoading: isLoadingEntitlements } = - useCheckEntitlements('assistant.advance_model') - - const selectedModel = useMemo(() => { - // While entitlements are loading, use the stored model without enforcing access - if (isLoadingEntitlements) { - return snap.model ?? DEFAULT_ASSISTANT_BASE_MODEL_ID - } - - const defaultModel = defaultAssistantModelId(hasAccessToAdvanceModel) - const model = snap.model ?? defaultModel - - if (!isKnownAssistantModelId(model)) return defaultModel - if (!hasAccessToAdvanceModel && !isAssistantBaseModelId(model)) { - return DEFAULT_ASSISTANT_BASE_MODEL_ID - } - - return model - }, [isLoadingEntitlements, hasAccessToAdvanceModel, snap.model]) - const [updatedOptInSinceMCP] = useLocalStorageQuery( LOCAL_STORAGE_KEYS.AI_ASSISTANT_MCP_OPT_IN, false @@ -652,7 +625,6 @@ export const AssistantChat = ({
)} - {disablePrompts && ( snap.setModel(model)} /> diff --git a/apps/studio/components/ui/AIAssistantPanel/AssistantChatForm.tsx b/apps/studio/components/ui/AIAssistantPanel/AssistantChatForm.tsx index eb92e6c157085..de4cbaf973041 100644 --- a/apps/studio/components/ui/AIAssistantPanel/AssistantChatForm.tsx +++ b/apps/studio/components/ui/AIAssistantPanel/AssistantChatForm.tsx @@ -46,9 +46,9 @@ export interface FormProps { /* If currently editing an existing message */ isEditing?: boolean /* The currently selected AI model */ - selectedModel: AssistantModelId + selectedModel?: AssistantModelId /* Callback when a model is chosen */ - onSelectModel: (model: AssistantModelId) => void + onSelectModel?: (model: AssistantModelId) => void } const AssistantChatFormComponent = forwardRef( @@ -99,6 +99,7 @@ const AssistantChatFormComponent = forwardRef( } const canSubmit = !disabled && !loading && !!value + const showModelSelector = selectedModel !== undefined && onSelectModel !== undefined return (
@@ -131,10 +132,17 @@ const AssistantChatFormComponent = forwardRef( onChange={(event) => onValueChange(event)} onKeyDown={handleKeyDown} /> -
-
- -
+
+ {showModelSelector && ( +
+ +
+ )}
{loading ? ( diff --git a/apps/studio/lib/ai/model.test.ts b/apps/studio/lib/ai/model.test.ts index e01e8131a7fda..85a1e7e940fe7 100644 --- a/apps/studio/lib/ai/model.test.ts +++ b/apps/studio/lib/ai/model.test.ts @@ -70,9 +70,8 @@ describe('getModel', () => { expect(error).toEqual(new Error('OPENAI_API_KEY not available')) }) - it('returns openai gpt-5.3-codex when hasAccessToAdvanceModel and not throttled', async () => { + it('returns openai gpt-5.3-codex with reasoning effort', async () => { vi.stubEnv('OPENAI_API_KEY', 'test-key') - vi.stubEnv('IS_THROTTLED', 'false') const { modelParams, error } = await getModel({ provider: 'openai', diff --git a/apps/studio/lib/ai/model.utils.test.ts b/apps/studio/lib/ai/model.utils.test.ts index 1d6d76edea0d7..8d7af1b987e3c 100644 --- a/apps/studio/lib/ai/model.utils.test.ts +++ b/apps/studio/lib/ai/model.utils.test.ts @@ -47,8 +47,9 @@ describe('model.utils', () => { it('should have openai provider with models', () => { expect(PROVIDERS.openai).toBeDefined() expect(PROVIDERS.openai.models).toBeDefined() - expect(Object.keys(PROVIDERS.openai.models)).toContain('gpt-5.3-codex') + expect(Object.keys(PROVIDERS.openai.models)).toContain('gpt-5.6-luna') expect(Object.keys(PROVIDERS.openai.models)).toContain('gpt-5.4-nano') + expect(Object.keys(PROVIDERS.openai.models)).toContain('gpt-5.3-codex') }) it('should have exactly one default model per provider', () => { @@ -111,20 +112,23 @@ describe('model.utils', () => { }) it('defaults should satisfy unions', () => { - expect(DEFAULT_ASSISTANT_BASE_MODEL_ID).toBe('gpt-5.4-nano') + expect(DEFAULT_ASSISTANT_BASE_MODEL_ID).toBe('gpt-5.6-luna') expect(DEFAULT_ASSISTANT_ADVANCE_MODEL_ID).toBe('gpt-5.3-codex') expect(defaultAssistantModelId(false)).toBe(DEFAULT_ASSISTANT_BASE_MODEL_ID) - expect(defaultAssistantModelId(true)).toBe(DEFAULT_ASSISTANT_ADVANCE_MODEL_ID) + expect(defaultAssistantModelId(true)).toBe(DEFAULT_ASSISTANT_BASE_MODEL_ID) }) it('isAssistantBaseModelId / isAdvanceOnlyModelId', () => { + expect(isAssistantBaseModelId('gpt-5.6-luna')).toBe(true) expect(isAssistantBaseModelId('gpt-5.4-nano')).toBe(true) expect(isAssistantBaseModelId('gpt-5.3-codex')).toBe(false) expect(isAdvanceOnlyModelId('gpt-5.3-codex')).toBe(true) expect(isAdvanceOnlyModelId('gpt-5.4-nano')).toBe(false) + expect(isAdvanceOnlyModelId('gpt-5.6-luna')).toBe(false) }) it('isKnownAssistantModelId', () => { + expect(isKnownAssistantModelId('gpt-5.6-luna')).toBe(true) expect(isKnownAssistantModelId('gpt-5.4-nano')).toBe(true) expect(isKnownAssistantModelId('gpt-5.3-codex')).toBe(true) expect(isKnownAssistantModelId('gpt-5')).toBe(false) @@ -133,28 +137,27 @@ describe('model.utils', () => { }) it('getAssistantModelEntry returns config for known ids', () => { + expect(getAssistantModelEntry('gpt-5.6-luna').reasoningEffort).toBe('medium') expect(getAssistantModelEntry('gpt-5.4-nano').reasoningEffort).toBe('low') expect(getAssistantModelEntry('gpt-5.3-codex').reasoningEffort).toBe('low') - expect(getAssistantModelEntry('gpt-5.4-nano')).toEqual( - ASSISTANT_MODELS.find((m) => m.id === 'gpt-5.4-nano') + expect(getAssistantModelEntry('gpt-5.6-luna')).toEqual( + ASSISTANT_MODELS.find((m) => m.id === 'gpt-5.6-luna') ) }) it('DEFAULT_COMPLETION_MODEL is gpt-5.4-nano with no reasoning effort', () => { - expect(DEFAULT_COMPLETION_MODEL.id).toBe(DEFAULT_ASSISTANT_BASE_MODEL_ID) + expect(DEFAULT_COMPLETION_MODEL.id).toBe('gpt-5.4-nano') expect(DEFAULT_COMPLETION_MODEL.reasoningEffort).toBe('none') }) it('openaiModelEntry enforces valid reasoning effort at compile time', () => { - // Valid: supported effort level const withEffort = openaiModelEntry({ - id: 'gpt-5.4-nano', + id: 'gpt-5.6-luna', reasoningEffort: 'low', }) expect(withEffort.reasoningEffort).toBe('low') - // Valid: no effort - const withoutEffort = openaiModelEntry({ id: 'gpt-5.4-nano' }) + const withoutEffort = openaiModelEntry({ id: 'gpt-5.6-luna' }) expect(withoutEffort.reasoningEffort).toBeUndefined() }) }) diff --git a/apps/studio/lib/ai/model.utils.ts b/apps/studio/lib/ai/model.utils.ts index 0174c2904ec82..ef83acc60b2fc 100644 --- a/apps/studio/lib/ai/model.utils.ts +++ b/apps/studio/lib/ai/model.utils.ts @@ -2,17 +2,19 @@ export type ProviderName = 'bedrock' | 'openai' export type BedrockModel = 'anthropic.claude-3-7-sonnet-20250219-v1:0' | 'openai.gpt-oss-120b-1:0' -export type OpenAIModelId = 'gpt-5.4-nano' | 'gpt-5.3-codex' +export type OpenAIModelId = 'gpt-5.4-nano' | 'gpt-5.3-codex' | 'gpt-5.6-luna' // Source: https://developers.openai.com/api/docs/guides/reasoning + per-model pages -export type ReasoningEffort = 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' +export type ReasoningEffort = 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max' // Per-model reasoning effort compatibility. // Sources: https://developers.openai.com/api/docs/models/gpt-5.4-nano // https://developers.openai.com/api/docs/models/gpt-5.3-codex +// https://developers.openai.com/api/docs/models/gpt-5.6-luna type ModelReasoningSupport = { 'gpt-5.4-nano': 'none' | 'low' | 'medium' | 'high' | 'xhigh' 'gpt-5.3-codex': 'low' | 'medium' | 'high' | 'xhigh' + 'gpt-5.6-luna': 'none' | 'low' | 'medium' | 'high' | 'xhigh' | 'max' } type ReasoningEffortFor = ModelId extends keyof ModelReasoningSupport @@ -59,6 +61,11 @@ export const LOGS_REWRITE_MODEL = openaiModelEntry({ // Single source of truth for all Assistant chat model variants and their reasoning levels. // Models with requiresAdvanceModelEntitlement false are available to all users; true requires the assistant.advance_model entitlement. export const ASSISTANT_MODELS = [ + openaiModelEntry({ + id: 'gpt-5.6-luna', + requiresAdvanceModelEntitlement: false, + reasoningEffort: 'medium', + }), openaiModelEntry({ id: 'gpt-5.4-nano', requiresAdvanceModelEntitlement: false, @@ -82,14 +89,12 @@ const ASSISTANT_MODELS_MAP = Object.fromEntries(ASSISTANT_MODELS.map((m) => [m.i (typeof ASSISTANT_MODELS)[number] > -export const DEFAULT_ASSISTANT_BASE_MODEL_ID = 'gpt-5.4-nano' satisfies AssistantBaseModelId +export const DEFAULT_ASSISTANT_BASE_MODEL_ID = 'gpt-5.6-luna' satisfies AssistantBaseModelId export const DEFAULT_ASSISTANT_ADVANCE_MODEL_ID = 'gpt-5.3-codex' satisfies AssistantModelId -export function defaultAssistantModelId(hasAccessToAdvanceModel: boolean): AssistantModelId { - return hasAccessToAdvanceModel - ? DEFAULT_ASSISTANT_ADVANCE_MODEL_ID - : DEFAULT_ASSISTANT_BASE_MODEL_ID +export function defaultAssistantModelId(_hasAccessToAdvanceModel: boolean): AssistantModelId { + return DEFAULT_ASSISTANT_BASE_MODEL_ID } export function isKnownAssistantModelId(id: string): id is AssistantModelId { @@ -153,6 +158,7 @@ export const PROVIDERS: ProviderRegistry = { }, openai: { models: { + 'gpt-5.6-luna': { default: false }, 'gpt-5.3-codex': { default: false }, 'gpt-5.4-nano': { default: true }, }, diff --git a/apps/studio/pages/api/ai/sql/generate-v4.ts b/apps/studio/pages/api/ai/sql/generate-v4.ts index bd16a812d6c81..b68ba949bc3a3 100644 --- a/apps/studio/pages/api/ai/sql/generate-v4.ts +++ b/apps/studio/pages/api/ai/sql/generate-v4.ts @@ -18,7 +18,6 @@ import { generateAssistantResponse } from '@/lib/ai/generate-assistant-response' import { isExplorerEnabled } from '@/lib/ai/is-explorer-enabled' import { getModel } from '@/lib/ai/model' import { - DEFAULT_ASSISTANT_ADVANCE_MODEL_ID, DEFAULT_ASSISTANT_BASE_MODEL_ID, getAssistantModelEntry, isAssistantBaseModelId, @@ -157,7 +156,7 @@ async function handlePost(req: NextApiRequest, res: NextApiResponse, claims?: Jw const envThrottled = process.env.IS_THROTTLED !== 'false' - let effectiveModel: AssistantModelId = requestedModel ?? DEFAULT_ASSISTANT_ADVANCE_MODEL_ID + let effectiveModel: AssistantModelId = requestedModel ?? DEFAULT_ASSISTANT_BASE_MODEL_ID if (!hasAccessToAdvanceModel || (envThrottled && !isAssistantBaseModelId(effectiveModel))) { effectiveModel = DEFAULT_ASSISTANT_BASE_MODEL_ID } diff --git a/apps/studio/state/ai-assistant-state.hooks.test.tsx b/apps/studio/state/ai-assistant-state.hooks.test.tsx index ed0e827ad3f5f..2dfc413e4e4cd 100644 --- a/apps/studio/state/ai-assistant-state.hooks.test.tsx +++ b/apps/studio/state/ai-assistant-state.hooks.test.tsx @@ -53,7 +53,6 @@ describe('useAiAssistantChatList', () => { state.loadPersistedState({ projectRef: 'default', activeChatId: 'persisted-chat', - model: state.model, chats: { 'persisted-chat': { id: 'persisted-chat', diff --git a/apps/studio/state/ai-assistant-state.test.ts b/apps/studio/state/ai-assistant-state.test.ts index 10dca9ca78d12..edf1d27c9adec 100644 --- a/apps/studio/state/ai-assistant-state.test.ts +++ b/apps/studio/state/ai-assistant-state.test.ts @@ -146,7 +146,6 @@ describe('whenAiAssistantInitialized', () => { state.loadPersistedState({ projectRef: 'default', activeChatId: 'persisted-chat', - model: state.model, chats: { 'persisted-chat': { id: 'persisted-chat',