mirror of
https://github.com/supabase/supabase.git
synced 2026-10-13 13:19:09 +03:00
We're exploring support for newer models like [gpt-5.4-nano](https://openai.com/index/introducing-gpt-5-4-mini-and-nano/) in Assistant. This model doesn't support the `'minimal'` reasoning effort level we use for gpt-5-mini which leads to vague errors. <img width="595" height="263" alt="CleanShot 2026-03-18 at 17 13 05@2x" src="https://github.com/user-attachments/assets/cf7c2370-322d-4a8a-be55-23e680db0aa0" /> Also, we've [previously discussed](https://supabase.slack.com/archives/C0161K73J1J/p1771544464850199?thread_ts=1771493920.775699&cid=C0161K73J1J) that reasoning adds unnecessary latency to otherwise simple AI completion endpoints like `title-v2`. We want more control of reasoning level independent of model/endpoint. This PR aims to solve both problems by: - making reasoning effort configurable on a per-request basis - adding compile-time guardrails to prevent selecting an incompatible reasoning level for models - adding a `DEFAULT_COMPLETION_MODEL` with minimal reasoning that we can update with newer models that support disabling reasoning (independent of Assistant chat model reasoning) Other improvements to our model config logic: - Fixes bug in `onboarding/design.ts` and `assistant.eval.ts` where `providerOptions` was being dropped - `getModel()` now returns a bundled `modelParams` object (spread into AI SDK calls) so `providerOptions` can't be accidentally omitted (this [has happened before](https://supabase.slack.com/archives/C0161K73J1J/p1771518443534309?thread_ts=1771493920.775699&cid=C0161K73J1J)) - Introduces an `ASSISTANT_MODELS` registry as a single source of truth for assistant model config, eliminating hardcoded model IDs across the codebase - Aligns free/pro model conditional logic with `assistant.advance_model` entitlement naming conventions instead of the `isLimited` pattern - Adds `console.error` logging of Assistant stream errors so we can interpret reasoning effort compatibility errors in the future (instead of just opaque "Sorry, I'm having trouble responding right now" card) - Removes unnecessary type casts and generally making the model config logic stricter - Removes pre-existing dead code: `anthropic` provider variant in `GetModelParams` / `PROVIDERS` registry that was never implemented in `getModel()` Now if you try to select an unsupported reasoning level you get a type error: <img width="1306" height="320" alt="CleanShot 2026-03-20 at 14 37 24@2x" src="https://github.com/user-attachments/assets/a6ac234b-5ea5-4d81-8e01-ac4be34a0800" /> And if for some reason an invalid reasoning level slips through, you now get a server-side error surfacing the issue: <img width="1268" height="204" alt="CleanShot 2026-03-20 at 14 58 14@2x" src="https://github.com/user-attachments/assets/aadc1b7a-9495-475f-9741-39979bd27cd7" /> I've tested gpt-5 and gpt-5-mini are still working on the staging preview and verified the models were selected properly in Braintrust logs. Both models are available on my Pro test account, and my Free test account shows the Pro upgrade CTA. Closes AI-446 Closes AI-551
167 lines
5.7 KiB
TypeScript
167 lines
5.7 KiB
TypeScript
import { generateText, Output, stepCountIs } from 'ai'
|
|
import { IS_PLATFORM } from 'common'
|
|
import { source } from 'common-tags'
|
|
import type { AiOptInLevel } from 'hooks/misc/useOrgOptedIntoAi'
|
|
import { getModel } from 'lib/ai/model'
|
|
import { DEFAULT_COMPLETION_MODEL } from 'lib/ai/model.utils'
|
|
import { getOrgAIDetails } from 'lib/ai/org-ai-details'
|
|
import { RLS_PROMPT } from 'lib/ai/prompts'
|
|
import { getTools } from 'lib/ai/tools'
|
|
import apiWrapper from 'lib/api/apiWrapper'
|
|
import { NextApiRequest, NextApiResponse } from 'next'
|
|
import { z } from 'zod'
|
|
|
|
const policySchema = z.object({
|
|
sql: z.string().describe('The generated Postgres CREATE POLICY statement.'),
|
|
name: z.string().describe('The name of the policy.'),
|
|
command: z
|
|
.enum(['SELECT', 'INSERT', 'UPDATE', 'DELETE', 'ALL'])
|
|
.describe('The SQL command this policy applies to.'),
|
|
definition: z
|
|
.string()
|
|
.optional()
|
|
.describe('The USING clause expression (for SELECT, UPDATE, DELETE).'),
|
|
check: z.string().optional().describe('The WITH CHECK clause expression (for INSERT, UPDATE).'),
|
|
action: z
|
|
.enum(['PERMISSIVE', 'RESTRICTIVE'])
|
|
.default('PERMISSIVE')
|
|
.describe('Whether the policy is PERMISSIVE or RESTRICTIVE.'),
|
|
roles: z.array(z.string()).default(['public']).describe('The roles this policy applies to.'),
|
|
})
|
|
|
|
const requestBodySchema = z.object({
|
|
tableName: z.string().min(1),
|
|
schema: z.string().default('public'),
|
|
columns: z.array(z.string()).optional(),
|
|
projectRef: z.string().min(1),
|
|
connectionString: z.string().min(1),
|
|
orgSlug: z.string().optional(),
|
|
message: z.string().optional(),
|
|
})
|
|
|
|
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|
const { method } = req
|
|
|
|
switch (method) {
|
|
case 'POST':
|
|
return handlePost(req, res)
|
|
default:
|
|
res.setHeader('Allow', ['POST'])
|
|
res.status(405).json({ data: null, error: { message: `Method ${method} Not Allowed` } })
|
|
}
|
|
}
|
|
|
|
export async function handlePost(req: NextApiRequest, res: NextApiResponse) {
|
|
const authorization = req.headers.authorization
|
|
const accessToken = authorization?.replace('Bearer ', '')
|
|
|
|
if (IS_PLATFORM && !accessToken) {
|
|
return res.status(401).json({ error: 'Authorization token is required' })
|
|
}
|
|
|
|
const body = typeof req.body === 'string' ? JSON.parse(req.body) : req.body
|
|
const { data, error: parseError } = requestBodySchema.safeParse(body)
|
|
|
|
if (parseError) {
|
|
return res.status(400).json({ error: 'Invalid request body', issues: parseError.issues })
|
|
}
|
|
|
|
const { tableName, schema, columns = [], projectRef, connectionString, orgSlug, message } = data
|
|
|
|
let aiOptInLevel: AiOptInLevel = 'disabled'
|
|
|
|
if (!IS_PLATFORM) {
|
|
aiOptInLevel = 'schema'
|
|
}
|
|
|
|
if (IS_PLATFORM && orgSlug && authorization) {
|
|
try {
|
|
const { aiOptInLevel: orgAIOptInLevel } = await getOrgAIDetails({
|
|
orgSlug,
|
|
authorization,
|
|
projectRef,
|
|
})
|
|
|
|
aiOptInLevel = orgAIOptInLevel
|
|
} catch (error) {
|
|
return res.status(400).json({
|
|
error: 'There was an error fetching your organization details',
|
|
})
|
|
}
|
|
}
|
|
|
|
try {
|
|
const { modelParams, error: modelError } = await getModel({
|
|
provider: 'openai',
|
|
modelEntry: DEFAULT_COMPLETION_MODEL,
|
|
})
|
|
|
|
if (modelError) {
|
|
return res.status(500).json({ error: modelError.message })
|
|
}
|
|
|
|
const tools = await getTools({
|
|
projectRef,
|
|
connectionString,
|
|
authorization,
|
|
aiOptInLevel,
|
|
accessToken,
|
|
})
|
|
|
|
const { experimental_output } = await generateText({
|
|
...modelParams,
|
|
stopWhen: stepCountIs(5),
|
|
prompt: source`
|
|
You are a Postgres RLS (Row Level Security) expert.
|
|
Determine the most appropriate policies for the "${schema}"."${tableName}" table within a Supabase project.
|
|
|
|
${columns.length > 0 ? `Table columns: ${columns.join(', ')}` : 'No column metadata provided.'}
|
|
|
|
${message ? `User request: ${message}` : ''}
|
|
|
|
RLS Guide: ${RLS_PROMPT}
|
|
|
|
Requirements:
|
|
- Use the available planning and schema tools (like "list_policies" or "list_tables") to inspect the "${schema}" schema and existing policies before generating new ones.
|
|
- Ensure policies strictly adhere to the existing schema
|
|
- Return a curated list of recommended CREATE POLICY statements as JSON.
|
|
- Each policy must include: name, sql, command (SELECT/INSERT/UPDATE/DELETE/ALL), action (PERMISSIVE/RESTRICTIVE), roles (array of role names).
|
|
- Include "definition" (USING clause expression without the USING keyword) for SELECT, UPDATE, DELETE policies.
|
|
- Include "check" (WITH CHECK clause expression without the WITH CHECK keywords) for INSERT, UPDATE policies.
|
|
- Avoid duplicating existing policies and reference the public schema and typical Supabase best practices when deciding the coverage.
|
|
- Prefer PERMISSIVE policies unless a RESTRICTIVE policy is explicitly required
|
|
`,
|
|
tools,
|
|
experimental_output: Output.object({
|
|
schema: z.object({
|
|
policies: z.array(policySchema),
|
|
}),
|
|
}),
|
|
})
|
|
|
|
// Add table and schema to each policy from the request
|
|
const policies = (experimental_output?.policies ?? []).map((policy) => ({
|
|
...policy,
|
|
table: tableName,
|
|
schema,
|
|
}))
|
|
|
|
return res.json(policies)
|
|
} catch (error) {
|
|
if (error instanceof Error) {
|
|
console.error(`AI policy generation failed: ${error.message}`)
|
|
return res.status(500).json({
|
|
error: 'Failed to generate policy. Please try again.',
|
|
})
|
|
}
|
|
return res.status(500).json({
|
|
error: 'An unknown error occurred.',
|
|
})
|
|
}
|
|
}
|
|
|
|
const wrapper = (req: NextApiRequest, res: NextApiResponse) =>
|
|
apiWrapper(req, res, handler, { withAuth: true })
|
|
|
|
export default wrapper
|