Skip to Content
AIModels

Models

NativeExpress connects to OpenAI, Anthropic and Google models through OpenRouter . Each AI feature sets its own models in its edge function’s config.json, which the app and the function both read.

@openrouter/ai-sdk-provider is imported inside the edge functions as a Deno npm: specifier. It is a server dependency and does not appear in the app’s package.json.

Where each feature’s models are set

FeatureFileKey
Chatsupabase/functions/chat/config.jsonmodels, the list the picker offers
Image generationsupabase/functions/generate/config.jsongenerateModel, one fixed model
Scansupabase/functions/identify/config.jsonidentifyModel, one fixed model

Each file is read by its feature in the app, through src/features/<feature>/config.ts, and by its edge function, so the two cannot disagree once the function is redeployed. supabase/functions/_utils/ai.config.json holds no models. It keeps only the settings every AI function shares: freeMessagesPerMonth and consentVersion.

Chat’s model list

supabase/functions/chat/config.json
{ "models": [ { "id": "openai/gpt-5.6-sol", "provider": "openai", "label": "GPT-5.6 Sol", "hostedTier": true, "vision": true }, { "id": "openai/gpt-5.6-luna", "provider": "openai", "label": "GPT-5.6 Luna", "hostedTier": true, "vision": true }, { "id": "anthropic/claude-sonnet-5", "provider": "anthropic", "label": "Claude Sonnet 5", "hostedTier": true, "vision": true }, { "id": "anthropic/claude-opus-4.8", "provider": "anthropic", "label": "Claude Opus 4.8", "hostedTier": false, "vision": true }, { "id": "google/gemini-3-flash-preview", "provider": "google", "label": "Gemini 3 Flash", "hostedTier": true, "vision": true }, { "id": "google/gemini-3.1-flash-image", "provider": "google", "label": "Gemini 3.1 Flash (Image)", "hostedTier": false, "vision": true, "imageOutput": true } ] }
FieldMeaning
idThe OpenRouter slug, sent to the provider as-is.
providerDisplay grouping in the picker only, not the routing.
labelWhat the user sees.
hostedTierfalse locks the model in the picker for free users. Tapping it opens the paywall. This flag controls the picker, not server authorization.
visionThe model accepts image input, so the attach button is available.
imageOutputThe model can return images. Always Pro-gated.

The client only ever sends a model id. The chat function checks it against MODELS, which supabase/functions/chat/registry.ts builds from this same file, so a request for a slug that was never in the picker is rejected before it reaches OpenRouter:

supabase/functions/chat/index.ts
const caps = MODELS.get(model); if (!caps) throw new RequestError('invalid_model', 400);

Redeploy chat after editing chat/config.json. Until then, the app’s picker can offer a model that the running function rejects with invalid_model.

Adding a chat model

Add a chat model to this project. Add it to the models array in supabase/functions/chat/config.json, then redeploy chat. Tell me which capability flags the model needs.

Find the slug

Browse openrouter.ai/models  and copy the exact slug. Check the model’s input and output modalities on that page, which is where vision and imageOutput come from.

Add it to chat/config.json

supabase/functions/chat/config.json
{ "id": "vendor/model-slug", "provider": "vendor", "label": "Model Name", "hostedTier": true, "vision": true }

Set hostedTier: false to lock it in the picker for free users. For a model with imageOutput: true, use hostedTier: false so the picker reflects the server’s Pro requirement.

The AI consent card names the companies that receive the user’s content, and it takes them from these model IDs. If the new model comes from a provider none of your models used before, bump consentVersion in supabase/functions/_utils/ai.config.json so every user is asked again. See AI consent.

Redeploy

supabase functions deploy chat

Verify

Check that the model appears in the picker and streams a reply. If you receive invalid_model, confirm the ID matches the entry in chat/config.json and redeploy chat.

Gating rules

The chat function applies these in order, before anything is sent to a provider:

  1. The user has not allowed AI at the current consent version → 403 ai_consent_required.
  2. Unknown slug → 400 invalid_model.
  3. Image attached to a non-vision model → 400 model_not_vision.
  4. An imageOutput model requested by a non-Pro user → 403 pro_required.
  5. Free quota exhausted and user is not Pro → 402 quota_exceeded.

hostedTier: false does not add a server-side Pro check for text models. If you need to restrict those models, add an entitlement check in supabase/functions/chat/index.ts before the provider call. Image-output models already have this check.

The server checks Pro status through RevenueCat using REVENUECAT_SECRET_KEY and the entitlement named by PRO_ENTITLEMENT_ID. If the key is unset, users cannot access Pro-only features. See Quota and paywall.

Using a different provider

The functions build their provider in supabase/functions/_utils/provider.ts:

supabase/functions/_utils/provider.ts
export function requireOpenRouter() { const apiKey = Deno.env.get('OPENROUTER_API_KEY'); if (!apiKey) throw new RequestError('provider_not_configured', 500); return createOpenRouter({ apiKey }); }
Switch this project's AI functions from OpenRouter to a direct provider. Update supabase/functions/_utils/provider.ts and its callers, set the new server-side secret, and update the model IDs in each function's config.json. Check chat, scan and image-generation support before changing the provider.

To use a direct provider, replace the provider factory and its secret lookup, then update its imports and model calls in chat, generate and identify. Update the model IDs in chat/config.json, generate/config.json and identify/config.json to match that provider.

Check image input, structured output and image generation separately. The functions currently use OpenRouter’s .chat() method and IMAGE_MODALITY_OPTIONS; another provider may need different methods or options. Follow the chosen provider’s AI SDK integration guide , then test each feature before deploying it.

Last updated on