diff --git a/apps/studio/evals/dataset.ts b/apps/studio/evals/dataset.ts index 65cc8081ad5..84da4f65264 100644 --- a/apps/studio/evals/dataset.ts +++ b/apps/studio/evals/dataset.ts @@ -109,7 +109,8 @@ export const dataset: AssistantEvalCase[] = [ { input: { prompt: 'Where can I go to create a support ticket?' }, expected: { - correctAnswer: 'https://supabase.com/dashboard/support/new', + correctAnswer: + 'https://supabase.com/dashboard/support/new (or https://supabase.help which redirects there)', }, metadata: { category: ['general_help'], diff --git a/apps/studio/lib/ai/model.test.ts b/apps/studio/lib/ai/model.test.ts index e8671bcf553..64f76f95e74 100644 --- a/apps/studio/lib/ai/model.test.ts +++ b/apps/studio/lib/ai/model.test.ts @@ -51,11 +51,11 @@ describe('getModel', () => { const { modelParams, promptProviderOptions } = await getModel({ provider: 'openai', - modelEntry: openaiModelEntry({ id: 'gpt-5-mini' }), + modelEntry: openaiModelEntry({ id: 'gpt-5.4-nano' }), }) expect(modelParams?.model).toEqual('openai-model') - expect(openai).toHaveBeenCalledWith('gpt-5-mini') + expect(openai).toHaveBeenCalledWith('gpt-5.4-nano') expect(promptProviderOptions).toBeUndefined() }) @@ -64,24 +64,24 @@ describe('getModel', () => { const { error } = await getModel({ provider: 'openai', - modelEntry: openaiModelEntry({ id: 'gpt-5-mini' }), + modelEntry: openaiModelEntry({ id: 'gpt-5.4-nano' }), }) expect(error).toEqual(new Error('OPENAI_API_KEY not available')) }) - it('returns openai gpt-5 when hasAccessToAdvanceModel and not throttled', async () => { + it('returns openai gpt-5.3-codex when hasAccessToAdvanceModel and not throttled', async () => { vi.stubEnv('OPENAI_API_KEY', 'test-key') vi.stubEnv('IS_THROTTLED', 'false') const { modelParams, error } = await getModel({ provider: 'openai', - modelEntry: openaiModelEntry({ id: 'gpt-5', reasoningEffort: 'minimal' }), + modelEntry: openaiModelEntry({ id: 'gpt-5.3-codex', reasoningEffort: 'low' }), }) expect(error).toBeUndefined() expect(modelParams?.model).toEqual('openai-model') - expect(openai).toHaveBeenCalledWith('gpt-5') - expect(modelParams?.providerOptions?.openai?.reasoningEffort).toBe('minimal') + expect(openai).toHaveBeenCalledWith('gpt-5.3-codex') + expect(modelParams?.providerOptions?.openai?.reasoningEffort).toBe('low') }) it('applies reasoningEffort from DEFAULT_COMPLETION_MODEL', async () => { @@ -93,7 +93,7 @@ describe('getModel', () => { }) expect(error).toBeUndefined() - expect(openai).toHaveBeenCalledWith('gpt-5-mini') - expect(modelParams?.providerOptions?.openai?.reasoningEffort).toBe('minimal') + expect(openai).toHaveBeenCalledWith('gpt-5.4-nano') + expect(modelParams?.providerOptions?.openai?.reasoningEffort).toBe('none') }) }) diff --git a/apps/studio/lib/ai/model.utils.test.ts b/apps/studio/lib/ai/model.utils.test.ts index 2de1e5dadf2..80fe423295a 100644 --- a/apps/studio/lib/ai/model.utils.test.ts +++ b/apps/studio/lib/ai/model.utils.test.ts @@ -25,7 +25,7 @@ describe('model.utils', () => { it('should return correct default for openai provider', () => { const result = getDefaultModelForProvider('openai') - expect(result).toBe('gpt-5-mini') + expect(result).toBe('gpt-5.4-nano') }) it('should return undefined for unknown provider', () => { @@ -47,8 +47,8 @@ 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') - expect(Object.keys(PROVIDERS.openai.models)).toContain('gpt-5-mini') + expect(Object.keys(PROVIDERS.openai.models)).toContain('gpt-5.3-codex') + expect(Object.keys(PROVIDERS.openai.models)).toContain('gpt-5.4-nano') }) it('should have exactly one default model per provider', () => { @@ -111,48 +111,50 @@ describe('model.utils', () => { }) it('defaults should satisfy unions', () => { - expect(DEFAULT_ASSISTANT_BASE_MODEL_ID).toBe('gpt-5-mini') - expect(DEFAULT_ASSISTANT_ADVANCE_MODEL_ID).toBe('gpt-5') + expect(DEFAULT_ASSISTANT_BASE_MODEL_ID).toBe('gpt-5.4-nano') + 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) }) it('isAssistantBaseModelId / isAdvanceOnlyModelId', () => { - expect(isAssistantBaseModelId('gpt-5-mini')).toBe(true) - expect(isAssistantBaseModelId('gpt-5')).toBe(false) - expect(isAdvanceOnlyModelId('gpt-5')).toBe(true) - expect(isAdvanceOnlyModelId('gpt-5-mini')).toBe(false) + 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) }) it('isKnownAssistantModelId', () => { - expect(isKnownAssistantModelId('gpt-5-mini')).toBe(true) - expect(isKnownAssistantModelId('gpt-5')).toBe(true) + expect(isKnownAssistantModelId('gpt-5.4-nano')).toBe(true) + expect(isKnownAssistantModelId('gpt-5.3-codex')).toBe(true) + expect(isKnownAssistantModelId('gpt-5')).toBe(false) + expect(isKnownAssistantModelId('gpt-5-mini')).toBe(false) expect(isKnownAssistantModelId('unknown')).toBe(false) }) it('getAssistantModelEntry returns config for known ids', () => { - expect(getAssistantModelEntry('gpt-5-mini').reasoningEffort).toBe('minimal') - expect(getAssistantModelEntry('gpt-5').reasoningEffort).toBe('minimal') - expect(getAssistantModelEntry('gpt-5-mini')).toEqual( - ASSISTANT_MODELS.find((m) => m.id === 'gpt-5-mini') + 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') ) }) - it('DEFAULT_COMPLETION_MODEL is gpt-5-mini with minimal reasoning effort', () => { + 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.reasoningEffort).toBe('minimal') + 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-mini', + id: 'gpt-5.4-nano', reasoningEffort: 'low', }) expect(withEffort.reasoningEffort).toBe('low') // Valid: no effort - const withoutEffort = openaiModelEntry({ id: 'gpt-5-mini' }) + const withoutEffort = openaiModelEntry({ id: 'gpt-5.4-nano' }) expect(withoutEffort.reasoningEffort).toBeUndefined() }) }) diff --git a/apps/studio/lib/ai/model.utils.ts b/apps/studio/lib/ai/model.utils.ts index 15dc0f8fabc..b558a1687b1 100644 --- a/apps/studio/lib/ai/model.utils.ts +++ b/apps/studio/lib/ai/model.utils.ts @@ -2,17 +2,17 @@ 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' | 'gpt-5-mini' +export type OpenAIModelId = 'gpt-5.4-nano' | 'gpt-5.3-codex' // Source: https://developers.openai.com/api/docs/guides/reasoning + per-model pages export type ReasoningEffort = 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' // Per-model reasoning effort compatibility. -// When adding a model, verify supported levels in the community matrix and add an entry: -// https://community.openai.com/t/request-for-compatibility-matrix-reasoning-effort-sampling-parameters-across-gpt-5-series/1371738/2 +// Sources: https://developers.openai.com/api/docs/models/gpt-5.4-nano +// https://developers.openai.com/api/docs/models/gpt-5.3-codex type ModelReasoningSupport = { - 'gpt-5': 'minimal' | 'low' | 'medium' | 'high' - 'gpt-5-mini': 'minimal' | 'low' | 'medium' | 'high' + 'gpt-5.4-nano': 'none' | 'low' | 'medium' | 'high' | 'xhigh' + 'gpt-5.3-codex': 'low' | 'medium' | 'high' | 'xhigh' } type ReasoningEffortFor = ModelId extends keyof ModelReasoningSupport @@ -47,22 +47,22 @@ export type OpenAIModelEntry = ReturnType /** Default model entry for simple completion endpoints where latency is more important than reasoning. */ export const DEFAULT_COMPLETION_MODEL = openaiModelEntry({ - id: 'gpt-5-mini', - reasoningEffort: 'minimal', + id: 'gpt-5.4-nano', + reasoningEffort: 'none', }) // 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-mini', + id: 'gpt-5.4-nano', requiresAdvanceModelEntitlement: false, - reasoningEffort: 'minimal', + reasoningEffort: 'low', }), openaiModelEntry({ - id: 'gpt-5', + id: 'gpt-5.3-codex', requiresAdvanceModelEntitlement: true, - reasoningEffort: 'minimal', + reasoningEffort: 'low', }), ] as const @@ -77,9 +77,9 @@ 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-mini' satisfies AssistantBaseModelId +export const DEFAULT_ASSISTANT_BASE_MODEL_ID = 'gpt-5.4-nano' satisfies AssistantBaseModelId -export const DEFAULT_ASSISTANT_ADVANCE_MODEL_ID = 'gpt-5' satisfies AssistantModelId +export const DEFAULT_ASSISTANT_ADVANCE_MODEL_ID = 'gpt-5.3-codex' satisfies AssistantModelId export function defaultAssistantModelId(hasAccessToAdvanceModel: boolean): AssistantModelId { return hasAccessToAdvanceModel @@ -148,8 +148,8 @@ export const PROVIDERS: ProviderRegistry = { }, openai: { models: { - 'gpt-5': { default: false }, - 'gpt-5-mini': { default: true }, + 'gpt-5.3-codex': { default: false }, + 'gpt-5.4-nano': { default: true }, }, providerOptions: { openai: { diff --git a/apps/studio/state/ai-assistant-state.tsx b/apps/studio/state/ai-assistant-state.tsx index 6cb44ef7197..5ac866e6065 100644 --- a/apps/studio/state/ai-assistant-state.tsx +++ b/apps/studio/state/ai-assistant-state.tsx @@ -8,6 +8,7 @@ import { proxy, ref, snapshot, subscribe, useSnapshot } from 'valtio' import { constructHeaders } from 'data/fetchers' import { prepareMessagesForAPI } from 'lib/ai/message-utils' +import { isKnownAssistantModelId } from 'lib/ai/model.utils' import type { AssistantModelId } from 'lib/ai/model.utils' import { BASE_PATH, IS_PLATFORM } from 'lib/constants' @@ -46,7 +47,7 @@ type AiAssistantData = { tables: { schema: string; name: string }[] chats: Record activeChatId?: string - model: AssistantModel + model?: AssistantModel context: AiAssistantContext } @@ -65,7 +66,7 @@ const INITIAL_AI_ASSISTANT: AiAssistantData = { tables: [], chats: {}, activeChatId: undefined, - model: 'gpt-5', + model: undefined, context: {}, } @@ -487,7 +488,11 @@ export const createAiAssistantState = (): AiAssistantState => { loadPersistedState: (persistedState: StoredAiAssistantState) => { state.chats = persistedState.chats state.activeChatId = persistedState.activeChatId - state.model = persistedState.model ?? INITIAL_AI_ASSISTANT.model + const storedModel = persistedState.model + state.model = + storedModel && isKnownAssistantModelId(storedModel) + ? storedModel + : INITIAL_AI_ASSISTANT.model // Ensure an active chat exists after loading if (!state.activeChat) {