From 6dee0757b320a6dcf4b94a4c0181971ecbd01eec Mon Sep 17 00:00:00 2001 From: Adam Date: Fri, 9 Oct 2026 17:00:04 +0000 Subject: [PATCH 1/7] Make Claude the default models and add the 5.5 family Add Claude Haiku 5.5 (Basic), Claude Opus 5.5 and Claude Sonnet 5.5 (Pro) to the catalog and retire Claude Sonnet 5 and Claude Opus 5. Haiku 5.5 is the Basic default and Opus 5.5 the Pro default. Auto Fast now prefers and falls back to Haiku; Auto Pro prefers and falls back to Opus. Older clients that still send a retired ID resolve to Opus 5.5 on the server and in the settings store instead of getting a 400. Co-Authored-By: Claude Opus 5.5 --- .../features/case-generation.md | 2 +- docs/architecture.md | 4 ++-- docs/testing.md | 4 ++-- e2e/production/canary.spec.ts | 10 ++++----- src/lib/stores/settings-store.ts | 3 ++- src/locales/en.json | 12 +++++------ src/locales/es.json | 12 +++++------ src/locales/hu.json | 12 +++++------ src/test/security-hardening.test.ts | 2 +- supabase/README.md | 2 +- .../_shared/auto-model-router.test.ts | 20 +++++++++--------- .../functions/_shared/auto-model-router.ts | 9 ++++---- .../functions/_shared/model-catalog.test.ts | 3 ++- supabase/functions/_shared/model-catalog.ts | 21 +++++++++++++++---- supabase/functions/ai-stream/handler_test.ts | 11 +++++----- supabase/functions/ai-stream/index.ts | 4 +++- .../functions/ai/handler-research.test.ts | 17 +++++++++------ supabase/functions/ai/index.ts | 4 +++- 18 files changed, 89 insertions(+), 63 deletions(-) diff --git a/.claude/skills/verify-debatelab/features/case-generation.md b/.claude/skills/verify-debatelab/features/case-generation.md index 6c83cee79..0078f0704 100644 --- a/.claude/skills/verify-debatelab/features/case-generation.md +++ b/.claude/skills/verify-debatelab/features/case-generation.md @@ -41,5 +41,5 @@ Preconditions: - `Save` appears both on the result toolbar and in the dialog. Scope the second click to `getByRole("dialog")`. The toolbar `Save` exists only for one-off results. - A case filed in a debate is titled with the motion, which the header control also shows (truncated and hidden). Look for it inside the History panel, not with a page-wide `getByText`. - The client files a result after it renders. Wait for the result on screen (and the row, if the next step reloads) before a `page.goto`, or the reload can drop it. -- The model label in the result header (for example `anthropic/claude-sonnet-5`) is the model the function requested. The answer itself came from the fake provider. +- The model label in the result header (for example `anthropic/claude-opus-5.5`) is the model the function requested. The answer itself came from the fake provider. - `Google Docs` export needs Google OAuth and cannot be proven locally. diff --git a/docs/architecture.md b/docs/architecture.md index bfc5088db..7bf331c69 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -128,9 +128,9 @@ Supabase Auth owns user sessions. Protected routes provide a client navigation b Checkout and portal functions create Stripe sessions after authenticated ownership and redirect validation. The webhook updates subscription state and records event processing for idempotency. AI handlers record the provider-reported usage and charge for each request. -All model requests use the single OpenRouter backend route and `OPENROUTER_API_KEY`. The shared ten-model catalog, Luna Basic default, Pro-only entries, and request controls live in [`supabase/functions/_shared/model-catalog.ts`](../supabase/functions/_shared/model-catalog.ts). OpenRouter usage remains raw in the audit row, and the reported charge is unknown when the provider does not return a complete cost. The opponent's voice is the one exception to the OpenRouter route: the `voice` function calls ElevenLabs with `ELEVENLABS_API_KEY`. ElevenLabs reports billed characters, not dollars, so [`supabase/functions/_shared/cost.ts`](../supabase/functions/_shared/cost.ts) prices those rows from a dated list-price snapshot; promotions and plan credits are not modelled, so a voice row can overstate the charge. +All model requests use the single OpenRouter backend route and `OPENROUTER_API_KEY`. The shared eleven-model catalog, its defaults (Claude Haiku 5.5 for Basic, Claude Opus 5.5 for Pro), Pro-only entries, retired-ID successors, and request controls live in [`supabase/functions/_shared/model-catalog.ts`](../supabase/functions/_shared/model-catalog.ts). OpenRouter usage remains raw in the audit row, and the reported charge is unknown when the provider does not return a complete cost. The opponent's voice is the one exception to the OpenRouter route: the `voice` function calls ElevenLabs with `ELEVENLABS_API_KEY`. ElevenLabs reports billed characters, not dollars, so [`supabase/functions/_shared/cost.ts`](../supabase/functions/_shared/cost.ts) prices those rows from a dated list-price snapshot; promotions and plan credits are not modelled, so a voice row can overstate the charge. -`Auto Fast` and `Auto Pro` are virtual selections, not provider model IDs. The Edge Functions resolve them through TypeSafe Jev on OpenRouter before calling the concrete writing model. Auto Fast is available to Basic accounts and normally resolves to GLM 5.3 Flash; manual GLM selection keeps its Pro restriction. Auto Pro remains Pro-only. Provider calls, usage rows, and response metadata record the concrete resolved model, while response metadata also retains the requested automatic selection. A Jev router call uses the same OpenRouter key and does not consume a second daily quota unit. +`Auto Fast` and `Auto Pro` are virtual selections, not provider model IDs. The Edge Functions resolve them through TypeSafe Jev on OpenRouter before calling the concrete writing model. Auto Fast is available to Basic accounts and normally resolves to Claude Haiku 5.5; it can also pick GLM 5.3 Flash, whose manual selection keeps its Pro restriction. Auto Pro remains Pro-only and normally resolves to Claude Opus 5.5. Provider calls, usage rows, and response metadata record the concrete resolved model, while response metadata also retains the requested automatic selection. A Jev router call uses the same OpenRouter key and does not consume a second daily quota unit. ## Observability diff --git a/docs/testing.md b/docs/testing.md index 6141d8969..50553abb9 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -288,8 +288,8 @@ npx playwright test --config=playwright.production.config.ts - every browser-facing Edge Function answers a preflight that allows the site origin (`workspace-invites` is excluded until production enables Workspaces); - `ai` returns 401, not a 5xx, to an unauthenticated call; - the canary account signs in, opens Generate, and Settings shows its Pro plan (a row-level-security read), without spending an AI request; -- the dedicated canary account gets a streamed GPT-5.6 Luna answer from `ai-stream`; -- the canary account generates a three-contention case with GPT-5.6 Luna through `ai`. +- the dedicated canary account gets a streamed Claude Haiku 5.5 answer from `ai-stream`; +- the canary account generates a three-contention case with Claude Haiku 5.5 through `ai`. The last two checks are the only production writes, and they touch only the canary account: its `ai_requests` usage rows and whatever the Generate page saves for it. The dedicated canary account has Pro promo access capped at 10 requests a day. Captures are disabled because they could retain its session. diff --git a/e2e/production/canary.spec.ts b/e2e/production/canary.spec.ts index b036e8c15..6a1686175 100644 --- a/e2e/production/canary.spec.ts +++ b/e2e/production/canary.spec.ts @@ -32,10 +32,10 @@ async function signInAsCanary(page: Page, { aiRequest }: { aiRequest: boolean }) }, data.session); } -// GPT-5.6 Luna is the Basic default, so this is the path most users take. -async function pickLuna(page: Page, feature: string) { +// Claude Haiku 5.5 is the Basic default, so this is the path most users take. +async function pickBasicDefault(page: Page, feature: string) { await page.getByRole("combobox", { name: `Select AI model for ${feature}` }).click(); - await page.getByRole("option", { name: /GPT-5\.6 Luna/ }).first().click(); + await page.getByRole("option", { name: /Claude Haiku 5\.5/ }).first().click(); } test("the production site loads and guards the app", async ({ page, baseURL }) => { @@ -89,7 +89,7 @@ test("the canary account signs in and loads its account", async ({ page }) => { test("the canary account gets a streamed answer from a real model", async ({ page }) => { await signInAsCanary(page, { aiRequest: true }); await page.goto("/app/ask"); - await pickLuna(page, "ask"); + await pickBasicDefault(page, "ask"); await page.getByPlaceholder("Ask your debate coach...").fill("Reply with one short sentence: what is a rebuttal?"); const stream = page.waitForResponse((response) => response.url().endsWith("/functions/v1/ai-stream"), { timeout: 90_000 }); await page.getByRole("button", { name: "Send message" }).click(); @@ -111,7 +111,7 @@ test("the canary account generates a case", async ({ page }) => { test.setTimeout(180_000); await signInAsCanary(page, { aiRequest: true }); await page.goto("/app/generate"); - await pickLuna(page, "generate"); + await pickBasicDefault(page, "generate"); await page.getByRole("textbox", { name: "Motion / Topic *" }).fill("This House would make public transport free"); const generated = page.waitForResponse((response) => response.url().endsWith("/functions/v1/ai") && response.request().method() === "POST", { timeout: 150_000 }); await page.getByRole("button", { name: "Generate Debate Case" }).click(); diff --git a/src/lib/stores/settings-store.ts b/src/lib/stores/settings-store.ts index ed9d5bb30..13c7dd505 100644 --- a/src/lib/stores/settings-store.ts +++ b/src/lib/stores/settings-store.ts @@ -10,6 +10,7 @@ import { getModelCatalogEntry, isAutoModelSelection, isReasoningEffort, + retiredModelSuccessor, } from '../../../supabase/functions/_shared/model-catalog'; import type { ProviderConfig } from '../ai/providers/types'; import { supabase } from '@/lib/supabase/client'; @@ -80,7 +81,7 @@ function normalizeModelId(value: unknown, fallback: string = DEFAULT_MODEL): str const model = value.trim(); if (isAutoModelSelection(model)) return model; if (getModelCatalogEntry(model)) return model; - return LEGACY_MODEL_SUCCESSORS[model] ?? DEFAULT_MODEL; + return retiredModelSuccessor(model) ?? LEGACY_MODEL_SUCCESSORS[model] ?? DEFAULT_MODEL; } function normalizeFeatureConfig( diff --git a/src/locales/en.json b/src/locales/en.json index 9aa0ab66e..18a72a0be 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -259,7 +259,7 @@ "factoidsTitle": "Factoids" }, "modelSelector": { - "autoFastDescription": "Fast routing, usually GLM", + "autoFastDescription": "Fast routing, usually Claude Haiku", "autoProDescription": "Routes among the strongest models", "reasoning": "Reasoning", "reasoningLow": "Low", @@ -932,7 +932,7 @@ "trialFeature4": "3-day access", "proFeature1": "Expanded AI prep access", "proFeature2": "All 11 prep modes", - "proFeature3": "All 10 models, including Sonnet, Opus, Muse Spark, and GLM Flash", + "proFeature3": "All 11 models, including Claude Opus and Sonnet, Muse Spark, and GLM Flash", "proFeature4": "Priority support", "proFeature5": "Active Grounding™ research", "startTrial": "Start Free Trial", @@ -946,9 +946,9 @@ "faq": { "title": "Frequently Asked Questions", "q1": "How much does DebateLab cost?", - "a1": "DebateLab offers a 3-day free trial. After that, Pro is $5/month + tax for expanded access and all 10 models.", + "a1": "DebateLab offers a 3-day free trial. After that, Pro is $5/month + tax for expanded access and all 11 models.", "q2": "What do I get with Pro?", - "a2": "Expanded AI prep access, all 10 models including Sonnet, Opus, Muse Spark, and GLM Flash, Active Grounding™ research, and priority support.", + "a2": "Expanded AI prep access, all 11 models including Claude Opus and Sonnet, Muse Spark, and GLM Flash, Active Grounding™ research, and priority support.", "q3": "Can I cancel my subscription?", "a3": "Yes, you can cancel anytime from your Settings page. You'll retain access until the end of your billing period.", "q4": "Does the AI make things up?", @@ -1356,7 +1356,7 @@ }, "pricingModal": { "title": "Upgrade to Pro", - "subtitle": "Unlock higher usage limits, all 10 models, and configurable reasoning.", + "subtitle": "Unlock higher usage limits, all 11 models, and configurable reasoning.", "free": "Free", "pro": "Pro", "current": "Current", @@ -1370,7 +1370,7 @@ "freeFeature3": "All debate features", "freeFeature4": "3-day free trial", "proFeature1": "Expanded AI prep access", - "proFeature2": "All 10 models, including Sonnet, Opus, Muse Spark, and GLM Flash", + "proFeature2": "All 11 models, including Claude Opus and Sonnet, Muse Spark, and GLM Flash", "proFeature3": "Deep research with Exa", "proFeature4": "Priority processing", "proFeature5": "All debate features", diff --git a/src/locales/es.json b/src/locales/es.json index 552f50135..9620a7da0 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -259,7 +259,7 @@ "factoidsTitle": "Datos" }, "modelSelector": { - "autoFastDescription": "Enrutamiento rápido, normalmente con GLM", + "autoFastDescription": "Enrutamiento rápido, normalmente con Claude Haiku", "autoProDescription": "Elige entre los modelos más capaces", "reasoning": "Razonamiento", "reasoningLow": "Bajo", @@ -932,7 +932,7 @@ "trialFeature4": "Acceso por 3 días", "proFeature1": "Acceso ampliado a preparación con IA", "proFeature2": "Las 11 herramientas de preparación", - "proFeature3": "Los 10 modelos, incluidos Sonnet, Opus, Muse Spark y GLM Flash", + "proFeature3": "Los 11 modelos, incluidos Claude Opus y Sonnet, Muse Spark y GLM Flash", "proFeature4": "Soporte prioritario", "proFeature5": "Investigación Active Grounding™", "startTrial": "Comenzar Prueba Gratis", @@ -946,9 +946,9 @@ "faq": { "title": "Preguntas Frecuentes", "q1": "¿Cuánto cuesta DebateLab?", - "a1": "DebateLab ofrece una prueba gratis de 3 días. Después, Pro cuesta $5/mes + impuestos para acceso ampliado y los 10 modelos.", + "a1": "DebateLab ofrece una prueba gratis de 3 días. Después, Pro cuesta $5/mes + impuestos para acceso ampliado y los 11 modelos.", "q2": "¿Qué obtengo con Pro?", - "a2": "Acceso ampliado a preparación con IA, los 10 modelos incluidos Sonnet, Opus, Muse Spark y GLM Flash, investigación Active Grounding™ y soporte prioritario.", + "a2": "Acceso ampliado a preparación con IA, los 11 modelos incluidos Claude Opus y Sonnet, Muse Spark y GLM Flash, investigación Active Grounding™ y soporte prioritario.", "q3": "¿Puedo cancelar mi suscripción?", "a3": "Sí, puedes cancelar en cualquier momento desde tu página de Ajustes. Mantendrás el acceso hasta el final de tu período de facturación.", "q4": "¿La IA inventa cosas?", @@ -1356,7 +1356,7 @@ }, "pricingModal": { "title": "Actualizar a Pro", - "subtitle": "Desbloquea límites de uso más altos, los 10 modelos y razonamiento configurable.", + "subtitle": "Desbloquea límites de uso más altos, los 11 modelos y razonamiento configurable.", "free": "Gratis", "pro": "Pro", "current": "Actual", @@ -1370,7 +1370,7 @@ "freeFeature3": "Todas las funciones de debate", "freeFeature4": "Prueba gratis de 3 días", "proFeature1": "Acceso ampliado a preparación con IA", - "proFeature2": "Los 10 modelos, incluidos Sonnet, Opus, Muse Spark y GLM Flash", + "proFeature2": "Los 11 modelos, incluidos Claude Opus y Sonnet, Muse Spark y GLM Flash", "proFeature3": "Investigación profunda con Exa", "proFeature4": "Procesamiento prioritario", "proFeature5": "Todas las funciones de debate", diff --git a/src/locales/hu.json b/src/locales/hu.json index 6cf3ba0fb..13d9e4a7c 100644 --- a/src/locales/hu.json +++ b/src/locales/hu.json @@ -259,7 +259,7 @@ "factoidsTitle": "Tények" }, "modelSelector": { - "autoFastDescription": "Gyors útválasztás, általában GLM-mel", + "autoFastDescription": "Gyors útválasztás, általában Claude Haikuval", "autoProDescription": "A legerősebb modellek közül választ", "reasoning": "Érvelés", "reasoningLow": "Alacsony", @@ -932,7 +932,7 @@ "trialFeature4": "3 napos hozzáférés", "proFeature1": "Bővített MI felkészülési hozzáférés", "proFeature2": "Mind a 11 felkészülési eszköz", - "proFeature3": "Mind a 10 modell, köztük a Sonnet, Opus, Muse Spark és GLM Flash", + "proFeature3": "Mind a 11 modell, köztük a Claude Opus és Sonnet, a Muse Spark és a GLM Flash", "proFeature4": "Elsőbbségi támogatás", "proFeature5": "Active Grounding™ kutatás", "startTrial": "Ingyenes Próba Indítása", @@ -946,9 +946,9 @@ "faq": { "title": "Gyakran Ismételt Kérdések", "q1": "Mennyibe kerül a DebateLab?", - "a1": "A DebateLab 3 napos ingyenes próbát kínál. Ezután a Pro $5/hó + adó bővített hozzáférést és mind a 10 modellt kínálja.", + "a1": "A DebateLab 3 napos ingyenes próbát kínál. Ezután a Pro $5/hó + adó bővített hozzáférést és mind a 11 modellt kínálja.", "q2": "Mit kapok a Pro-val?", - "a2": "Bővített MI felkészülési hozzáférés mind a 10 modellel, köztük a Sonnet, Opus, Muse Spark és GLM Flash modellekkel, Active Grounding™ kutatással és elsőbbségi támogatással.", + "a2": "Bővített MI felkészülési hozzáférés mind a 11 modellel, köztük a Claude Opus és Sonnet, a Muse Spark és a GLM Flash modellekkel, Active Grounding™ kutatással és elsőbbségi támogatással.", "q3": "Lemondhatom az előfizetésem?", "a3": "Igen, bármikor lemondhatod a Beállítások oldalon. A számlázási időszak végéig megtartod a hozzáférést.", "q4": "Az MI kitalál dolgokat?", @@ -1356,7 +1356,7 @@ }, "pricingModal": { "title": "Frissítés Pro-ra", - "subtitle": "Oldd fel a magasabb használati keretet, mind a 10 modellt és a beállítható érvelést.", + "subtitle": "Oldd fel a magasabb használati keretet, mind a 11 modellt és a beállítható érvelést.", "free": "Ingyenes", "pro": "Pro", "current": "Jelenlegi", @@ -1370,7 +1370,7 @@ "freeFeature3": "Minden vitafunkció", "freeFeature4": "3 napos ingyenes próba", "proFeature1": "Bővített MI felkészülési hozzáférés", - "proFeature2": "Mind a 10 modell, köztük a Sonnet, Opus, Muse Spark és GLM Flash", + "proFeature2": "Mind a 11 modell, köztük a Claude Opus és Sonnet, a Muse Spark és a GLM Flash", "proFeature3": "Mélyreható kutatás Exa-val", "proFeature4": "Elsőbbségi feldolgozás", "proFeature5": "Minden vitafunkció", diff --git a/src/test/security-hardening.test.ts b/src/test/security-hardening.test.ts index e960eda45..81a543966 100644 --- a/src/test/security-hardening.test.ts +++ b/src/test/security-hardening.test.ts @@ -75,7 +75,7 @@ describe('security hardening regressions', () => { it('uses allowlisted model/provider pairs and SSRF-safe citation verification', () => { expect(modelCatalog).toContain('id: "z-ai/glm-5.3-flash"'); - expect(modelCatalog).toContain('id: "anthropic/claude-sonnet-5"'); + expect(modelCatalog).toContain('id: "anthropic/claude-haiku-5.5"'); expect(modelCatalog).toContain('id: "openai/gpt-5.6-luna"'); expect(modelCatalog).toContain('id: "google/gemini-3.8-flash"'); expect(ai).toContain('function modelForRequest'); diff --git a/supabase/README.md b/supabase/README.md index bac85dcb0..f3ea3b4b2 100644 --- a/supabase/README.md +++ b/supabase/README.md @@ -36,7 +36,7 @@ supabase/tests/ pgTAP database and policy tests Both endpoints accept an optional `debateContext` of IDs (`debateId`, `side`, `sourceIds`, `outputs`). `_shared/debate-context-loader.ts` loads the matching material with the service role, filtering every query by the caller's user ID and the one debate, and returns 404 for a debate the caller does not own before any quota is reserved. Requests without it still accept the legacy `sourceIds` field. -Both endpoints route every model inference through OpenRouter and enforce the shared ten-model Basic/Pro catalog. GPT-5.6 Luna is the Basic default; Pro-only models require active Pro access. +Both endpoints route every model inference through OpenRouter and enforce the shared eleven-model Basic/Pro catalog. Claude Haiku 5.5 is the Basic default and Claude Opus 5.5 the Pro default; Pro-only models require active Pro access. Retired catalog IDs from older clients resolve to their successor before validation. The browser boundary is [`../src/lib/ai/client.ts`](../src/lib/ai/client.ts). Shared prompt, research, source-quality, cost, entitlement, CORS, and control logic lives in `_shared`. diff --git a/supabase/functions/_shared/auto-model-router.test.ts b/supabase/functions/_shared/auto-model-router.test.ts index b6b0f4b52..8d67abb47 100644 --- a/supabase/functions/_shared/auto-model-router.test.ts +++ b/supabase/functions/_shared/auto-model-router.test.ts @@ -29,15 +29,15 @@ function jevReturning(result: JevDecisionResult, observed?: { calls: number; sta }; } -Deno.test("Auto Fast normally resolves to GLM for a Basic user", async () => { +Deno.test("Auto Fast normally resolves to Claude Haiku for a Basic user", async () => { const resolved = await resolveModelSelection({ requestedModel: AUTO_FAST, feature: "generate", input: { motion: "This house would subsidize public transit", language: "en" }, canAccessProModels: false, - callJevImpl: jevReturning(choiceResult("z-ai/glm-5.3-flash")), + callJevImpl: jevReturning(choiceResult("anthropic/claude-haiku-5.5")), }); - if (resolved.requestedModel !== AUTO_FAST || resolved.resolvedModel !== "z-ai/glm-5.3-flash" || resolved.usedFallback) { + if (resolved.requestedModel !== AUTO_FAST || resolved.resolvedModel !== "anthropic/claude-haiku-5.5" || resolved.usedFallback) { throw new Error(`unexpected Auto Fast resolution: ${JSON.stringify(resolved)}`); } }); @@ -55,7 +55,7 @@ Deno.test("Auto Fast can route logic-heavy work to DeepSeek Flash", async () => } }); -Deno.test("Auto Fast falls back to GLM when Jev is uncertain or unavailable", async () => { +Deno.test("Auto Fast falls back to Claude Haiku when Jev is uncertain or unavailable", async () => { const uncertain = await resolveModelSelection({ requestedModel: AUTO_FAST, feature: "ask", @@ -63,7 +63,7 @@ Deno.test("Auto Fast falls back to GLM when Jev is uncertain or unavailable", as canAccessProModels: false, callJevImpl: jevReturning(choiceResult("openai/gpt-5.6-luna", 0.54)), }); - if (uncertain.resolvedModel !== "z-ai/glm-5.3-flash" || !uncertain.usedFallback) throw new Error("uncertain route did not fall back"); + if (uncertain.resolvedModel !== "anthropic/claude-haiku-5.5" || !uncertain.usedFallback) throw new Error("uncertain route did not fall back"); const unavailable = await resolveModelSelection({ requestedModel: AUTO_FAST, @@ -72,7 +72,7 @@ Deno.test("Auto Fast falls back to GLM when Jev is uncertain or unavailable", as canAccessProModels: false, callJevImpl: () => Promise.reject(Object.assign(new Error("provider unavailable"), { usage: { inputTokens: 9, outputTokens: 0 } })), }); - if (unavailable.resolvedModel !== "z-ai/glm-5.3-flash" || !unavailable.usedFallback) throw new Error("failed route did not fall back"); + if (unavailable.resolvedModel !== "anthropic/claude-haiku-5.5" || !unavailable.usedFallback) throw new Error("failed route did not fall back"); if (unavailable.jevUsage?.inputTokens !== 9) throw new Error("failed route usage was discarded"); }); @@ -82,9 +82,9 @@ Deno.test("Auto Fast ignores a Jev label outside its candidate set", async () => feature: "generate", input: { motion: "Should cities ban cars?" }, canAccessProModels: false, - callJevImpl: jevReturning(choiceResult("anthropic/claude-opus-5")), + callJevImpl: jevReturning(choiceResult("anthropic/claude-opus-5.5")), }); - if (resolved.resolvedModel !== "z-ai/glm-5.3-flash" || !resolved.usedFallback) throw new Error("unsupported label escaped candidate gate"); + if (resolved.resolvedModel !== "anthropic/claude-haiku-5.5" || !resolved.usedFallback) throw new Error("unsupported label escaped candidate gate"); }); Deno.test("Auto Pro rejects Basic before calling Jev", async () => { @@ -106,7 +106,7 @@ Deno.test("Auto Pro rejects Basic before calling Jev", async () => { }); Deno.test("Auto Pro accepts only its approved smart-model pool", async () => { - for (const model of ["openai/gpt-5.6-sol", "anthropic/claude-opus-5", "anthropic/claude-sonnet-5"]) { + for (const model of ["anthropic/claude-opus-5.5", "anthropic/claude-sonnet-5.5", "openai/gpt-5.6-sol"]) { const resolved = await resolveModelSelection({ requestedModel: AUTO_PRO, feature: "generate", @@ -124,7 +124,7 @@ Deno.test("Auto Pro accepts only its approved smart-model pool", async () => { canAccessProModels: true, callJevImpl: jevReturning(choiceResult("meta/muse-spark-1.3")), }); - if (outsidePool.resolvedModel !== "anthropic/claude-sonnet-5" || !outsidePool.usedFallback) { + if (outsidePool.resolvedModel !== "anthropic/claude-opus-5.5" || !outsidePool.usedFallback) { throw new Error("Auto Pro accepted a model outside its pool"); } }); diff --git a/supabase/functions/_shared/auto-model-router.ts b/supabase/functions/_shared/auto-model-router.ts index 1cb0358c4..46608e5bf 100644 --- a/supabase/functions/_shared/auto-model-router.ts +++ b/supabase/functions/_shared/auto-model-router.ts @@ -18,16 +18,17 @@ const ROUTING_CONFIDENCE = 0.55; const MAX_ROUTING_STATE_CHARS = 8_000; const FAST_CANDIDATES = { - "z-ai/glm-5.3-flash": "Default for ordinary structured DebateLab work and efficient long-context requests.", + "anthropic/claude-haiku-5.5": "Default for ordinary DebateLab work: cases, rebuttals, coaching, and structured output.", + "z-ai/glm-5.3-flash": "Best for efficient long-context requests that are mostly extraction or summarizing.", "openai/gpt-5.6-luna": "Best for short latency-sensitive conversation and simple coaching.", "google/gemini-3.8-flash": "Best for very long input, multilingual output, or broad context handling.", "deepseek/deepseek-v4-flash-0731": "Best for logic-heavy analysis, adversarial attack, or structured reasoning at low cost.", } as const; const PRO_CANDIDATES = { + "anthropic/claude-opus-5.5": "Default for professional DebateLab work: nuanced rhetoric, adversarial critique, and long-form writing.", + "anthropic/claude-sonnet-5.5": "Best for quick, simpler Pro requests where speed matters more than depth.", "openai/gpt-5.6-sol": "Best for complex structured reasoning and reliable multi-part analysis.", - "anthropic/claude-opus-5": "Best for nuanced rhetoric, adversarial critique, and demanding long-form writing.", - "anthropic/claude-sonnet-5": "Balanced choice for professional work when no specialist clearly wins.", } as const; type AutoCandidate = keyof typeof FAST_CANDIDATES | keyof typeof PRO_CANDIDATES; @@ -73,7 +74,7 @@ function routingState(args: ResolveModelSelectionArgs) { } function fallbackFor(selection: typeof AUTO_FAST | typeof AUTO_PRO): AutoCandidate { - return selection === AUTO_FAST ? "z-ai/glm-5.3-flash" : "anthropic/claude-sonnet-5"; + return selection === AUTO_FAST ? "anthropic/claude-haiku-5.5" : "anthropic/claude-opus-5.5"; } function isAbort(error: unknown, signal?: AbortSignal): boolean { diff --git a/supabase/functions/_shared/model-catalog.test.ts b/supabase/functions/_shared/model-catalog.test.ts index f18ab5ea5..51242b727 100644 --- a/supabase/functions/_shared/model-catalog.test.ts +++ b/supabase/functions/_shared/model-catalog.test.ts @@ -19,6 +19,7 @@ Deno.test("every catalog model supports the forced trial effort and both default Deno.test("model catalog marks only the approved Pro entries as Pro-only", () => { const basic = new Set([ + "anthropic/claude-haiku-5.5", "openai/gpt-5.6-luna", "google/gemini-3.8-flash", "deepseek/deepseek-v4-flash-0731", @@ -41,7 +42,7 @@ Deno.test("trial and unsupported Pro reasoning resolve server-side", () => { if (effectiveReasoningEffort("openai/gpt-5.6-luna", "high", false) !== "low") { throw new Error("trial request did not force low"); } - if (effectiveReasoningEffort("anthropic/claude-sonnet-5", "medium", true) !== "medium") { + if (effectiveReasoningEffort("anthropic/claude-opus-5.5", "medium", true) !== "medium") { throw new Error("native Pro medium was changed"); } for (const model of ["deepseek/deepseek-v4-flash-0731", "deepseek/deepseek-v4-pro-0813", "z-ai/glm-5.3-flash"]) { diff --git a/supabase/functions/_shared/model-catalog.ts b/supabase/functions/_shared/model-catalog.ts index 7cc8ae81e..8841e745f 100644 --- a/supabase/functions/_shared/model-catalog.ts +++ b/supabase/functions/_shared/model-catalog.ts @@ -15,11 +15,12 @@ export interface ModelCatalogEntry { } export const MODEL_CATALOG = [ + { id: "anthropic/claude-haiku-5.5", name: "Claude Haiku 5.5", maxTokens: 128_000, isProOnly: false, supportedEfforts: ["low", "medium", "high"] }, { id: "openai/gpt-5.6-luna", name: "GPT-5.6 Luna", maxTokens: 128_000, isProOnly: false, supportedEfforts: ["low", "medium", "high"] }, { id: "google/gemini-3.8-flash", name: "Gemini 3.8 Flash", maxTokens: 65_536, isProOnly: false, supportedEfforts: ["low", "medium", "high"] }, { id: "deepseek/deepseek-v4-flash-0731", name: "DeepSeek V4 Flash", maxTokens: 131_072, isProOnly: false, supportedEfforts: ["low", "high"] }, - { id: "anthropic/claude-sonnet-5", name: "Claude Sonnet 5", maxTokens: 128_000, isProOnly: true, supportedEfforts: ["low", "medium", "high"] }, - { id: "anthropic/claude-opus-5", name: "Claude Opus 5", maxTokens: 128_000, isProOnly: true, supportedEfforts: ["low", "medium", "high"] }, + { id: "anthropic/claude-opus-5.5", name: "Claude Opus 5.5", maxTokens: 128_000, isProOnly: true, supportedEfforts: ["low", "medium", "high"] }, + { id: "anthropic/claude-sonnet-5.5", name: "Claude Sonnet 5.5", maxTokens: 128_000, isProOnly: true, supportedEfforts: ["low", "medium", "high"] }, { id: "openai/gpt-5.6-sol", name: "GPT-5.6 Sol", maxTokens: 128_000, isProOnly: true, supportedEfforts: ["low", "medium", "high"] }, { id: "openai/gpt-5.6-terra", name: "GPT-5.6 Terra", maxTokens: 128_000, isProOnly: true, supportedEfforts: ["low", "medium", "high"] }, { id: "deepseek/deepseek-v4-pro-0813", name: "DeepSeek V4 Pro", maxTokens: 384_000, isProOnly: true, supportedEfforts: ["low", "high"] }, @@ -37,8 +38,16 @@ export const AUTO_MODEL_SELECTIONS = [ export type AutoModelSelection = (typeof AUTO_MODEL_SELECTIONS)[number]["id"]; export type ModelSelection = ModelId | AutoModelSelection; -export const DEFAULT_MODEL = "openai/gpt-5.6-luna" as const; -export const DEFAULT_PRO_MODEL = "anthropic/claude-sonnet-5" as const; +export const DEFAULT_MODEL = "anthropic/claude-haiku-5.5" as const; +export const DEFAULT_PRO_MODEL = "anthropic/claude-opus-5.5" as const; + +// Retired catalog IDs that older clients (an installed PWA, a saved setting) +// may still send. Both the server and the settings store resolve them here. +// Sonnet 5 was the Pro default, so its users move to the new Pro default. +const RETIRED_MODEL_SUCCESSORS = new Map([ + ["anthropic/claude-sonnet-5", DEFAULT_PRO_MODEL], + ["anthropic/claude-opus-5", DEFAULT_PRO_MODEL], +]); const MODEL_BY_ID = new Map(MODEL_CATALOG.map((entry) => [entry.id, entry])); @@ -50,6 +59,10 @@ export function isAutoModelSelection(value: unknown): value is AutoModelSelectio return value === AUTO_FAST || value === AUTO_PRO; } +export function retiredModelSuccessor(model: string): ModelId | undefined { + return RETIRED_MODEL_SUCCESSORS.get(model); +} + export function isModelSelection(value: unknown): value is ModelSelection { return typeof value === "string" && (isAutoModelSelection(value) || MODEL_BY_ID.has(value)); } diff --git a/supabase/functions/ai-stream/handler_test.ts b/supabase/functions/ai-stream/handler_test.ts index 853556c43..38ce3eb94 100644 --- a/supabase/functions/ai-stream/handler_test.ts +++ b/supabase/functions/ai-stream/handler_test.ts @@ -103,13 +103,14 @@ Deno.test("streaming Ask forwards the shared prompt and returns provider SSE", a answers: { model: { type: "choice", - choice: "z-ai/glm-5.3-flash", + choice: "anthropic/claude-haiku-5.5", confidence: 0.96, probabilities: { - "z-ai/glm-5.3-flash": 0.96, + "anthropic/claude-haiku-5.5": 0.96, + "z-ai/glm-5.3-flash": 0.01, "openai/gpt-5.6-luna": 0.02, "google/gemini-3.8-flash": 0.01, - "deepseek/deepseek-v4-flash-0731": 0.01, + "deepseek/deepseek-v4-flash-0731": 0.0, }, }, }, @@ -154,10 +155,10 @@ Deno.test("streaming Ask forwards the shared prompt and returns provider SSE", a assertIncludes(body, "event: final"); assertIncludes(body, '"response":"Szia Adam"'); assertIncludes(body, '"requestedModel":"auto-fast"'); - assertIncludes(body, '"resolvedModel":"z-ai/glm-5.3-flash"'); + assertIncludes(body, '"resolvedModel":"anthropic/claude-haiku-5.5"'); const messages = providerRequest?.messages as Array<{ role: string; content: string }> | undefined; - if (providerRequest?.model !== "z-ai/glm-5.3-flash") { + if (providerRequest?.model !== "anthropic/claude-haiku-5.5") { throw new Error(`unexpected OpenRouter model: ${String(providerRequest?.model)}`); } const streamOptions = providerRequest?.stream_options as { include_usage?: unknown } | undefined; diff --git a/supabase/functions/ai-stream/index.ts b/supabase/functions/ai-stream/index.ts index 34d940e7c..c90a502bb 100644 --- a/supabase/functions/ai-stream/index.ts +++ b/supabase/functions/ai-stream/index.ts @@ -31,6 +31,7 @@ import { isModelSelection, isReasoningEffort, MAX_REQUEST_OUTPUT_TOKENS, + retiredModelSuccessor, type ModelCatalogEntry, type ModelSelection, type ReasoningEffort, @@ -122,7 +123,8 @@ export async function handleRequest(req: Request): Promise { return jsonResponse({ error: "Streaming currently supports Ask only" }, 400, corsHeaders); } - const rawModel = sanitizeText(body.model, 80) || DEFAULT_MODEL; + const sentModel = sanitizeText(body.model, 80) || DEFAULT_MODEL; + const rawModel = retiredModelSuccessor(sentModel) ?? sentModel; if (!isModelSelection(rawModel)) return jsonResponse({ error: "Unsupported model or provider combination" }, 400, corsHeaders); const requestedModel: ModelSelection = rawModel; const requestedReasoningEffort = body.reasoningEffort === undefined diff --git a/supabase/functions/ai/handler-research.test.ts b/supabase/functions/ai/handler-research.test.ts index 9e4d03478..b40d21ab6 100644 --- a/supabase/functions/ai/handler-research.test.ts +++ b/supabase/functions/ai/handler-research.test.ts @@ -655,7 +655,7 @@ Deno.test("AI handlers use strict OpenRouter routing and server-side reasoning t decisionBodies.push(body); const questions = body.questions as Record }>; const labels = Object.keys(questions.model.criteria); - const choice = labels.includes("z-ai/glm-5.3-flash") ? "z-ai/glm-5.3-flash" : "openai/gpt-5.6-sol"; + const choice = labels.includes("anthropic/claude-haiku-5.5") ? "anthropic/claude-haiku-5.5" : "anthropic/claude-opus-5.5"; return jsonResponse({ model: "typesafe/jev-1.13", answers: { @@ -689,7 +689,7 @@ Deno.test("AI handlers use strict OpenRouter routing and server-side reasoning t if (response.status !== 200) throw new Error(`${entry.id} returned ${response.status}: ${await response.text()}`); } for (const effort of ["low", "medium", "high"]) { - const response = await requestForModel("anthropic/claude-sonnet-5", "pro", effort); + const response = await requestForModel("anthropic/claude-sonnet-5.5", "pro", effort); if (response.status !== 200) throw new Error(`Pro ${effort} returned ${response.status}: ${await response.text()}`); } for (const model of ["deepseek/deepseek-v4-flash-0731", "deepseek/deepseek-v4-pro-0813", "z-ai/glm-5.3-flash"]) { @@ -702,7 +702,7 @@ Deno.test("AI handlers use strict OpenRouter routing and server-side reasoning t const autoFastBasic = await requestForModel("auto-fast", "trial"); if (autoFastBasic.status !== 200) throw new Error(`Basic Auto Fast returned ${autoFastBasic.status}: ${await autoFastBasic.text()}`); const autoFastPayload = await autoFastBasic.json(); - if (autoFastPayload.meta?.requestedModel !== "auto-fast" || autoFastPayload.meta?.resolvedModel !== "z-ai/glm-5.3-flash") { + if (autoFastPayload.meta?.requestedModel !== "auto-fast" || autoFastPayload.meta?.resolvedModel !== "anthropic/claude-haiku-5.5") { throw new Error(`Auto Fast metadata was wrong: ${JSON.stringify(autoFastPayload.meta)}`); } const autoProBasic = await requestForModel("auto-pro", "trial"); @@ -710,7 +710,7 @@ Deno.test("AI handlers use strict OpenRouter routing and server-side reasoning t const autoPro = await requestForModel("auto-pro", "pro"); if (autoPro.status !== 200) throw new Error(`Pro Auto Pro returned ${autoPro.status}: ${await autoPro.text()}`); const autoProPayload = await autoPro.json(); - if (autoProPayload.meta?.resolvedModel !== "openai/gpt-5.6-sol") { + if (autoProPayload.meta?.resolvedModel !== "anthropic/claude-opus-5.5") { throw new Error(`Auto Pro metadata was wrong: ${JSON.stringify(autoProPayload.meta)}`); } @@ -739,8 +739,13 @@ Deno.test("AI handlers use strict OpenRouter routing and server-side reasoning t const body = openRouterBodies[MODEL_CATALOG.length + offset]; if ((body.reasoning as Record).effort !== "low") throw new Error("unsupported Pro medium was not lowered"); } - if (openRouterBodies.at(-2)?.model !== "z-ai/glm-5.3-flash") throw new Error("Auto Fast did not call GLM"); - if (openRouterBodies.at(-1)?.model !== "openai/gpt-5.6-sol") throw new Error("Auto Pro did not call Sol"); + if (openRouterBodies.at(-2)?.model !== "anthropic/claude-haiku-5.5") throw new Error("Auto Fast did not call Haiku"); + if (openRouterBodies.at(-1)?.model !== "anthropic/claude-opus-5.5") throw new Error("Auto Pro did not call Opus"); + + // A stale client still sending a retired ID reaches its successor, not a 400. + const retired = await requestForModel("anthropic/claude-sonnet-5", "pro"); + if (retired.status !== 200) throw new Error(`retired Sonnet 5 returned ${retired.status}: ${await retired.text()}`); + if (openRouterBodies.at(-1)?.model !== "anthropic/claude-opus-5.5") throw new Error("retired Sonnet 5 did not reach Opus 5.5"); } finally { globalThis.fetch = originalFetch; if (originalTier === undefined) delete testState.__testTier; diff --git a/supabase/functions/ai/index.ts b/supabase/functions/ai/index.ts index 34281ee4a..1dc4e85f2 100644 --- a/supabase/functions/ai/index.ts +++ b/supabase/functions/ai/index.ts @@ -30,6 +30,7 @@ import { effectiveReasoningEffort, isModelSelection, isReasoningEffort, + retiredModelSuccessor, type ModelCatalogEntry, type ModelSelection, } from "../_shared/model-catalog.ts"; @@ -145,7 +146,8 @@ export async function handleRequest(req: Request): Promise { } const feature = typeof body.feature === "string" ? body.feature as Feature : undefined; const rawInput = body.input; - const rawModel = (typeof body.model === "string" ? body.model : "") || DEFAULT_MODEL; + const sentModel = (typeof body.model === "string" ? body.model : "") || DEFAULT_MODEL; + const rawModel = retiredModelSuccessor(sentModel) ?? sentModel; if (!isModelSelection(rawModel)) { log(requestId, "WARN", "Rejected unsupported model/provider combination", { model: rawModel }); return badRequest("Unsupported model or provider combination", corsHeaders); From 6f556bb171a8cb400cfa57572542e737bfbff865 Mon Sep 17 00:00:00 2001 From: Adam Date: Fri, 9 Oct 2026 17:05:55 +0000 Subject: [PATCH 2/7] Add a verify recipe for the model defaults Co-Authored-By: Claude Opus 5.5 --- .../verify-debatelab/features/README.md | 1 + .../features/model-defaults.md | 24 +++++ .../recipes/model-defaults.spec.ts | 95 +++++++++++++++++++ 3 files changed, 120 insertions(+) create mode 100644 .claude/skills/verify-debatelab/features/model-defaults.md create mode 100644 .claude/skills/verify-debatelab/recipes/model-defaults.spec.ts diff --git a/.claude/skills/verify-debatelab/features/README.md b/.claude/skills/verify-debatelab/features/README.md index 89c97d1a2..9884fed72 100644 --- a/.claude/skills/verify-debatelab/features/README.md +++ b/.claude/skills/verify-debatelab/features/README.md @@ -44,6 +44,7 @@ Each feature file starts with an H1 title and one paragraph describing the user- - [Library sources](./library-sources.md): add, pause, delete, grounding, and URL import (`recipes/library.spec.ts`). - [AI tools](./ai-tools.md): Attack, Brainstorm, Factoids, and Ask streamed, cited, and with web search (`recipes/ai-tools.spec.ts`). - [Current Debate](./current-debate.md): start, the first-debate offer and the different-motion choice, sources and results scoped to a debate, switching, and the first onboarding step (`recipes/current-debate.spec.ts`); debate context review and pins (`e2e/full-stack/debate-context.spec.ts`). +- [Model defaults](./model-defaults.md): Pro accounts start on Claude Opus 5.5, Basic accounts on Claude Haiku 5.5 with Pro models locked, and retired model IDs move to Opus 5.5 (`recipes/model-defaults.spec.ts`). - [Practice](./practice.md): a full Lincoln-Douglas sparring round with verdict, and Crossfire in both directions with debrief, each filed in its debate (`recipes/practice.spec.ts`). - [Voice rounds](./voice-rounds.md): spoken Sparring and Crossfire on the browser voice, with no ElevenLabs key (`recipes/voice-rounds.spec.ts`). - [Workspace autosave](./workspace-autosave.md): a slow Crossfire or Sparring save from a Workspace never overwrites newer text (`recipes/workspace-autosave.spec.ts`). Needs `up --collab`; skips without it. diff --git a/.claude/skills/verify-debatelab/features/model-defaults.md b/.claude/skills/verify-debatelab/features/model-defaults.md new file mode 100644 index 000000000..8965fe19c --- /dev/null +++ b/.claude/skills/verify-debatelab/features/model-defaults.md @@ -0,0 +1,24 @@ +# Model defaults and the model selector + +Every AI tool has a model selector. A new Pro account starts every tool on Claude Opus 5.5, and a Basic (trial) account on Claude Haiku 5.5. Pro-only models show a `Pro` badge and are disabled for Basic accounts. A setting saved with a retired catalog ID (Claude Sonnet 5 or Claude Opus 5) is rewritten to Claude Opus 5.5 on the next load, and the Edge Functions accept the retired ID from an older client and route it to Opus 5.5. The catalog and defaults live in `supabase/functions/_shared/model-catalog.ts`. + +## Sub-features + +- `model-pro-default`: a new Pro account's `user_settings.feature_configs` hold `anthropic/claude-opus-5.5` for every tool, the selector shows `Claude Opus 5.5`, and a Generate run records `model: anthropic/claude-opus-5.5` in `ai_requests`. +- `model-basic-default`: a trial account with no saved settings gets `anthropic/claude-haiku-5.5` everywhere. `Claude Opus 5.5` and `Claude Sonnet 5.5` are disabled options, and a Generate run records Haiku 5.5. +- `model-retired`: saved `anthropic/claude-sonnet-5` configs become `anthropic/claude-opus-5.5` on screen and in `user_settings`. + +## How to get to it (user POV) + +- Any tool page, for example `/app/generate`: the combobox named `Select AI model for generate` sits beside the tool's inputs. + +## Driving it with verify.sh + Playwright + +- `bin/verify.sh drive recipes/model-defaults` covers all three sub-features. +- Demo sign-in grants Pro. For the Basic path the recipe sets `subscription_tier: "trial"`, a future `trial_ends_at`, clears `grace_period_ends_at` and `daily_limit_override`, and empties `feature_configs`, then reloads so the client re-hydrates as a new trial account. +- End states are read from `user_settings.feature_configs` and `ai_requests.model` through `admin`, and from the fake provider's recorded `model`. + +## Gotchas + +- Fake mode records the requested model but answers with fake text, and live mode rewrites every chat call to the Muse Spark contributor pin. Neither proves that OpenRouter accepts the Claude 5.5 IDs; that needs a direct OpenRouter call. +- Existing saved choices of a model still in the catalog are kept. Only retired IDs and unsaved settings move to the new defaults. diff --git a/.claude/skills/verify-debatelab/recipes/model-defaults.spec.ts b/.claude/skills/verify-debatelab/recipes/model-defaults.spec.ts new file mode 100644 index 000000000..d4e3f5c5c --- /dev/null +++ b/.claude/skills/verify-debatelab/recipes/model-defaults.spec.ts @@ -0,0 +1,95 @@ +import type { Page } from "@playwright/test"; +import { admin, AI_TIMEOUT, aiResponse, chooseForMotion, expect, providerCalls, test } from "./fixtures"; + +// Feature map: features/model-defaults.md. +const motion = "This House would make public transport free at the point of use"; +const OPUS = "anthropic/claude-opus-5.5"; +const HAIKU = "anthropic/claude-haiku-5.5"; + +async function settings(userId: string) { + const { data, error } = await admin.from("user_settings").select("feature_configs").eq("user_id", userId).single(); + if (error) throw error; + return data.feature_configs as Record; +} + +async function updateSettings(userId: string, values: Record) { + const { error } = await admin.from("user_settings").update(values).eq("user_id", userId); + if (error) throw error; +} + +async function generateOnce(page: Page) { + await page.getByRole("textbox", { name: "Motion / Topic *" }).fill(motion); + const response = aiResponse(page, "generate"); + await page.getByRole("button", { name: "Generate Debate Case" }).click(); + await chooseForMotion(page, "once"); + return response; +} + +test("model-pro-default: a new Pro account starts on Claude Opus 5.5 and generates with it", async ({ signedIn: page, user, evidence }) => { + const userId = await user.userId(); + await expect.poll(async () => (await settings(userId)).generate?.model, { timeout: AI_TIMEOUT }).toBe(OPUS); + + await page.goto("/app/generate"); + const selector = page.getByRole("combobox", { name: "Select AI model for generate" }); + await expect(selector).toContainText("Claude Opus 5.5"); + await selector.click(); + await expect(page.getByRole("option", { name: /Claude Haiku 5\.5/ })).toBeEnabled(); + await expect(page.getByRole("option", { name: /Claude Sonnet 5\.5/ })).toBeEnabled(); + await evidence.shot("01-pro-model-list"); + await page.keyboard.press("Escape"); + await evidence.step("New Pro account: selector shows Claude Opus 5.5, all Claude 5.5 models enabled"); + + const body = await generateOnce(page); + expect((body.meta as { resolvedModel?: string } | undefined)?.resolvedModel ?? OPUS).toBe(OPUS); + const calls = await providerCalls(); + expect(calls.filter((call) => call.feature === "generate").map((call) => call.model)).toContain(OPUS); + const { data: usage } = await admin.from("ai_requests").select("feature,status,model").eq("user_id", userId); + expect(usage).toContainEqual(expect.objectContaining({ feature: "generate", status: "success", model: OPUS })); + await evidence.json("02-ai-requests", usage); + await evidence.shot("02-generated-with-opus"); +}); + +test("model-basic-default: a Basic account starts on Claude Haiku 5.5 and Pro models stay locked", async ({ signedIn: page, user, evidence }) => { + const userId = await user.userId(); + // Demo sign-in grants Pro; turn the account into a fresh trial with nothing saved. + await updateSettings(userId, { + subscription_tier: "trial", + trial_ends_at: new Date(Date.now() + 2 * 86_400_000).toISOString(), + grace_period_ends_at: null, + daily_limit_override: null, + feature_configs: {}, + }); + await page.goto("/app/generate"); + await expect.poll(async () => (await settings(userId)).generate?.model, { timeout: AI_TIMEOUT }).toBe(HAIKU); + + const selector = page.getByRole("combobox", { name: "Select AI model for generate" }); + await expect(selector).toContainText("Claude Haiku 5.5"); + await selector.click(); + await expect(page.getByRole("option", { name: /Claude Opus 5\.5/ })).toBeDisabled(); + await expect(page.getByRole("option", { name: /Claude Sonnet 5\.5/ })).toBeDisabled(); + await evidence.shot("01-basic-model-list"); + await page.keyboard.press("Escape"); + await evidence.step("Basic account: selector shows Claude Haiku 5.5, Opus and Sonnet 5.5 locked behind Pro"); + + await generateOnce(page); + const { data: usage } = await admin.from("ai_requests").select("feature,status,model").eq("user_id", userId); + expect(usage).toContainEqual(expect.objectContaining({ feature: "generate", status: "success", model: HAIKU })); + await evidence.json("02-ai-requests", usage); +}); + +test("model-retired: a saved Claude Sonnet 5 setting moves to Claude Opus 5.5", async ({ signedIn: page, user, evidence }) => { + const userId = await user.userId(); + const retired = Object.fromEntries(["generate", "attack", "brainstorm", "factoids", "ask", "sparring"].map((feature) => [ + feature, + { provider: "openrouter", model: "anthropic/claude-sonnet-5", temperature: 0.7, reasoningEffort: "low" }, + ])); + await updateSettings(userId, { feature_configs: retired }); + await page.evaluate(() => localStorage.removeItem("debate-trainer-settings")); + await page.goto("/app/generate"); + + await expect(page.getByRole("combobox", { name: "Select AI model for generate" })).toContainText("Claude Opus 5.5"); + await expect.poll(async () => Object.values(await settings(userId)).map((config) => config.model), { timeout: AI_TIMEOUT }) + .toEqual(Array(6).fill(OPUS)); + await evidence.shot("01-retired-migrated"); + await evidence.step("Saved Sonnet 5 configs were rewritten to Opus 5.5 in user_settings"); +}); From 3b045332b081f64cbb549eabd2e8a9c53164fc5c Mon Sep 17 00:00:00 2001 From: Adam Date: Fri, 9 Oct 2026 17:14:51 +0000 Subject: [PATCH 3/7] Move the rest of the catalog to each family's newest model GPT-5.6 Luna becomes GPT-6 Luna, GPT-5.6 Sol becomes GPT-6.1 Sol, and DeepSeek V4 Flash becomes DeepSeek V4.1 Flash. GPT-6 has no Terra, so GPT-5.6 Terra retires into GPT-6.1 Sol. The catalog is back to ten models. Retired IDs resolve in the settings store and at both Edge Function boundaries. GPT-5.6 Luna, the old Basic default, moves to the new Basic default, Claude Haiku 5.5. A retired Basic ID never resolves to a Pro model. Co-Authored-By: Claude Opus 5.5 --- .../features/model-defaults.md | 2 +- docs/architecture.md | 2 +- docs/testing.md | 2 +- scripts/prompt-examples.ts | 2 +- src/locales/en.json | 10 +++---- src/locales/es.json | 10 +++---- src/locales/hu.json | 10 +++---- src/test/ai/streaming.test.ts | 16 +++++----- src/test/debate/debate-context.test.tsx | 2 +- src/test/security-hardening.test.ts | 2 +- supabase/README.md | 2 +- .../_shared/auto-model-router.test.ts | 10 +++---- .../functions/_shared/auto-model-router.ts | 6 ++-- .../functions/_shared/entitlements.test.ts | 2 +- .../functions/_shared/model-catalog.test.ts | 21 ++++++++++--- supabase/functions/_shared/model-catalog.ts | 14 +++++---- supabase/functions/ai-stream/auth_test.ts | 2 +- supabase/functions/ai-stream/handler_test.ts | 6 ++-- supabase/functions/ai-stream/index_test.ts | 2 +- supabase/functions/ai/debate-context.test.ts | 2 +- .../functions/ai/generate-honesty_test.ts | 4 +-- .../functions/ai/handler-research.test.ts | 30 +++++++++---------- supabase/functions/ai/voice.test.ts | 2 +- 23 files changed, 89 insertions(+), 72 deletions(-) diff --git a/.claude/skills/verify-debatelab/features/model-defaults.md b/.claude/skills/verify-debatelab/features/model-defaults.md index 8965fe19c..80d98d955 100644 --- a/.claude/skills/verify-debatelab/features/model-defaults.md +++ b/.claude/skills/verify-debatelab/features/model-defaults.md @@ -1,6 +1,6 @@ # Model defaults and the model selector -Every AI tool has a model selector. A new Pro account starts every tool on Claude Opus 5.5, and a Basic (trial) account on Claude Haiku 5.5. Pro-only models show a `Pro` badge and are disabled for Basic accounts. A setting saved with a retired catalog ID (Claude Sonnet 5 or Claude Opus 5) is rewritten to Claude Opus 5.5 on the next load, and the Edge Functions accept the retired ID from an older client and route it to Opus 5.5. The catalog and defaults live in `supabase/functions/_shared/model-catalog.ts`. +Every AI tool has a model selector. A new Pro account starts every tool on Claude Opus 5.5, and a Basic (trial) account on Claude Haiku 5.5. Pro-only models show a `Pro` badge and are disabled for Basic accounts. A setting saved with a retired catalog ID is rewritten on the next load: GPT-5.6 Luna moves to Claude Haiku 5.5, Claude Sonnet 5 and Opus 5 to Claude Opus 5.5, GPT-5.6 Sol and Terra to GPT-6.1 Sol, and DeepSeek V4 Flash to DeepSeek V4.1 Flash. The Edge Functions resolve a retired ID from an older client the same way. A retired Basic ID never resolves to a Pro model. The catalog and defaults live in `supabase/functions/_shared/model-catalog.ts`. ## Sub-features diff --git a/docs/architecture.md b/docs/architecture.md index 7bf331c69..6348242a4 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -128,7 +128,7 @@ Supabase Auth owns user sessions. Protected routes provide a client navigation b Checkout and portal functions create Stripe sessions after authenticated ownership and redirect validation. The webhook updates subscription state and records event processing for idempotency. AI handlers record the provider-reported usage and charge for each request. -All model requests use the single OpenRouter backend route and `OPENROUTER_API_KEY`. The shared eleven-model catalog, its defaults (Claude Haiku 5.5 for Basic, Claude Opus 5.5 for Pro), Pro-only entries, retired-ID successors, and request controls live in [`supabase/functions/_shared/model-catalog.ts`](../supabase/functions/_shared/model-catalog.ts). OpenRouter usage remains raw in the audit row, and the reported charge is unknown when the provider does not return a complete cost. The opponent's voice is the one exception to the OpenRouter route: the `voice` function calls ElevenLabs with `ELEVENLABS_API_KEY`. ElevenLabs reports billed characters, not dollars, so [`supabase/functions/_shared/cost.ts`](../supabase/functions/_shared/cost.ts) prices those rows from a dated list-price snapshot; promotions and plan credits are not modelled, so a voice row can overstate the charge. +All model requests use the single OpenRouter backend route and `OPENROUTER_API_KEY`. The shared ten-model catalog, its defaults (Claude Haiku 5.5 for Basic, Claude Opus 5.5 for Pro), Pro-only entries, retired-ID successors, and request controls live in [`supabase/functions/_shared/model-catalog.ts`](../supabase/functions/_shared/model-catalog.ts). OpenRouter usage remains raw in the audit row, and the reported charge is unknown when the provider does not return a complete cost. The opponent's voice is the one exception to the OpenRouter route: the `voice` function calls ElevenLabs with `ELEVENLABS_API_KEY`. ElevenLabs reports billed characters, not dollars, so [`supabase/functions/_shared/cost.ts`](../supabase/functions/_shared/cost.ts) prices those rows from a dated list-price snapshot; promotions and plan credits are not modelled, so a voice row can overstate the charge. `Auto Fast` and `Auto Pro` are virtual selections, not provider model IDs. The Edge Functions resolve them through TypeSafe Jev on OpenRouter before calling the concrete writing model. Auto Fast is available to Basic accounts and normally resolves to Claude Haiku 5.5; it can also pick GLM 5.3 Flash, whose manual selection keeps its Pro restriction. Auto Pro remains Pro-only and normally resolves to Claude Opus 5.5. Provider calls, usage rows, and response metadata record the concrete resolved model, while response metadata also retains the requested automatic selection. A Jev router call uses the same OpenRouter key and does not consume a second daily quota unit. diff --git a/docs/testing.md b/docs/testing.md index 50553abb9..5d111b275 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -86,7 +86,7 @@ OPENROUTER_API_KEY=... deno run --allow-read --allow-write --allow-env=OPENROUTE --allow-net=openrouter.ai scripts/prompt-examples.ts supabase/functions /tmp/after.json ``` -This sends the fourteen prompt examples to one real model (GPT-5.6 Luna by default) and writes the raw outputs for manual review. Point the first argument at a directory holding an older `_shared/prompts.ts` and `ai/prompt-builder.ts` to compare revisions. It runs no web research and is a bounded manual check, not a model matrix or an automated quality gate. +This sends the fourteen prompt examples to one real model (GPT-6 Luna by default) and writes the raw outputs for manual review. Point the first argument at a directory holding an older `_shared/prompts.ts` and `ai/prompt-builder.ts` to compare revisions. It runs no web research and is a bounded manual check, not a model matrix or an automated quality gate. ### Live Jev contract smoke diff --git a/scripts/prompt-examples.ts b/scripts/prompt-examples.ts index 87a8d9fab..84ab08622 100644 --- a/scripts/prompt-examples.ts +++ b/scripts/prompt-examples.ts @@ -11,7 +11,7 @@ import { PROMPT_EXAMPLES } from "../supabase/functions/_shared/prompts/prompt-examples.ts"; -const [functionsDir, outPath, model = "openai/gpt-5.6-luna", only] = Deno.args; +const [functionsDir, outPath, model = "openai/gpt-6-luna", only] = Deno.args; const selected = only ? new Set(only.split(",")) : null; if (!functionsDir || !outPath) { console.error("usage: prompt-examples.ts [model]"); diff --git a/src/locales/en.json b/src/locales/en.json index 18a72a0be..a987ef41e 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -932,7 +932,7 @@ "trialFeature4": "3-day access", "proFeature1": "Expanded AI prep access", "proFeature2": "All 11 prep modes", - "proFeature3": "All 11 models, including Claude Opus and Sonnet, Muse Spark, and GLM Flash", + "proFeature3": "All 10 models, including Claude Opus and Sonnet, Muse Spark, and GLM Flash", "proFeature4": "Priority support", "proFeature5": "Active Grounding™ research", "startTrial": "Start Free Trial", @@ -946,9 +946,9 @@ "faq": { "title": "Frequently Asked Questions", "q1": "How much does DebateLab cost?", - "a1": "DebateLab offers a 3-day free trial. After that, Pro is $5/month + tax for expanded access and all 11 models.", + "a1": "DebateLab offers a 3-day free trial. After that, Pro is $5/month + tax for expanded access and all 10 models.", "q2": "What do I get with Pro?", - "a2": "Expanded AI prep access, all 11 models including Claude Opus and Sonnet, Muse Spark, and GLM Flash, Active Grounding™ research, and priority support.", + "a2": "Expanded AI prep access, all 10 models including Claude Opus and Sonnet, Muse Spark, and GLM Flash, Active Grounding™ research, and priority support.", "q3": "Can I cancel my subscription?", "a3": "Yes, you can cancel anytime from your Settings page. You'll retain access until the end of your billing period.", "q4": "Does the AI make things up?", @@ -1356,7 +1356,7 @@ }, "pricingModal": { "title": "Upgrade to Pro", - "subtitle": "Unlock higher usage limits, all 11 models, and configurable reasoning.", + "subtitle": "Unlock higher usage limits, all 10 models, and configurable reasoning.", "free": "Free", "pro": "Pro", "current": "Current", @@ -1370,7 +1370,7 @@ "freeFeature3": "All debate features", "freeFeature4": "3-day free trial", "proFeature1": "Expanded AI prep access", - "proFeature2": "All 11 models, including Claude Opus and Sonnet, Muse Spark, and GLM Flash", + "proFeature2": "All 10 models, including Claude Opus and Sonnet, Muse Spark, and GLM Flash", "proFeature3": "Deep research with Exa", "proFeature4": "Priority processing", "proFeature5": "All debate features", diff --git a/src/locales/es.json b/src/locales/es.json index 9620a7da0..3eafbc716 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -932,7 +932,7 @@ "trialFeature4": "Acceso por 3 días", "proFeature1": "Acceso ampliado a preparación con IA", "proFeature2": "Las 11 herramientas de preparación", - "proFeature3": "Los 11 modelos, incluidos Claude Opus y Sonnet, Muse Spark y GLM Flash", + "proFeature3": "Los 10 modelos, incluidos Claude Opus y Sonnet, Muse Spark y GLM Flash", "proFeature4": "Soporte prioritario", "proFeature5": "Investigación Active Grounding™", "startTrial": "Comenzar Prueba Gratis", @@ -946,9 +946,9 @@ "faq": { "title": "Preguntas Frecuentes", "q1": "¿Cuánto cuesta DebateLab?", - "a1": "DebateLab ofrece una prueba gratis de 3 días. Después, Pro cuesta $5/mes + impuestos para acceso ampliado y los 11 modelos.", + "a1": "DebateLab ofrece una prueba gratis de 3 días. Después, Pro cuesta $5/mes + impuestos para acceso ampliado y los 10 modelos.", "q2": "¿Qué obtengo con Pro?", - "a2": "Acceso ampliado a preparación con IA, los 11 modelos incluidos Claude Opus y Sonnet, Muse Spark y GLM Flash, investigación Active Grounding™ y soporte prioritario.", + "a2": "Acceso ampliado a preparación con IA, los 10 modelos incluidos Claude Opus y Sonnet, Muse Spark y GLM Flash, investigación Active Grounding™ y soporte prioritario.", "q3": "¿Puedo cancelar mi suscripción?", "a3": "Sí, puedes cancelar en cualquier momento desde tu página de Ajustes. Mantendrás el acceso hasta el final de tu período de facturación.", "q4": "¿La IA inventa cosas?", @@ -1356,7 +1356,7 @@ }, "pricingModal": { "title": "Actualizar a Pro", - "subtitle": "Desbloquea límites de uso más altos, los 11 modelos y razonamiento configurable.", + "subtitle": "Desbloquea límites de uso más altos, los 10 modelos y razonamiento configurable.", "free": "Gratis", "pro": "Pro", "current": "Actual", @@ -1370,7 +1370,7 @@ "freeFeature3": "Todas las funciones de debate", "freeFeature4": "Prueba gratis de 3 días", "proFeature1": "Acceso ampliado a preparación con IA", - "proFeature2": "Los 11 modelos, incluidos Claude Opus y Sonnet, Muse Spark y GLM Flash", + "proFeature2": "Los 10 modelos, incluidos Claude Opus y Sonnet, Muse Spark y GLM Flash", "proFeature3": "Investigación profunda con Exa", "proFeature4": "Procesamiento prioritario", "proFeature5": "Todas las funciones de debate", diff --git a/src/locales/hu.json b/src/locales/hu.json index 13d9e4a7c..5ab77703f 100644 --- a/src/locales/hu.json +++ b/src/locales/hu.json @@ -932,7 +932,7 @@ "trialFeature4": "3 napos hozzáférés", "proFeature1": "Bővített MI felkészülési hozzáférés", "proFeature2": "Mind a 11 felkészülési eszköz", - "proFeature3": "Mind a 11 modell, köztük a Claude Opus és Sonnet, a Muse Spark és a GLM Flash", + "proFeature3": "Mind a 10 modell, köztük a Claude Opus és Sonnet, a Muse Spark és a GLM Flash", "proFeature4": "Elsőbbségi támogatás", "proFeature5": "Active Grounding™ kutatás", "startTrial": "Ingyenes Próba Indítása", @@ -946,9 +946,9 @@ "faq": { "title": "Gyakran Ismételt Kérdések", "q1": "Mennyibe kerül a DebateLab?", - "a1": "A DebateLab 3 napos ingyenes próbát kínál. Ezután a Pro $5/hó + adó bővített hozzáférést és mind a 11 modellt kínálja.", + "a1": "A DebateLab 3 napos ingyenes próbát kínál. Ezután a Pro $5/hó + adó bővített hozzáférést és mind a 10 modellt kínálja.", "q2": "Mit kapok a Pro-val?", - "a2": "Bővített MI felkészülési hozzáférés mind a 11 modellel, köztük a Claude Opus és Sonnet, a Muse Spark és a GLM Flash modellekkel, Active Grounding™ kutatással és elsőbbségi támogatással.", + "a2": "Bővített MI felkészülési hozzáférés mind a 10 modellel, köztük a Claude Opus és Sonnet, a Muse Spark és a GLM Flash modellekkel, Active Grounding™ kutatással és elsőbbségi támogatással.", "q3": "Lemondhatom az előfizetésem?", "a3": "Igen, bármikor lemondhatod a Beállítások oldalon. A számlázási időszak végéig megtartod a hozzáférést.", "q4": "Az MI kitalál dolgokat?", @@ -1356,7 +1356,7 @@ }, "pricingModal": { "title": "Frissítés Pro-ra", - "subtitle": "Oldd fel a magasabb használati keretet, mind a 11 modellt és a beállítható érvelést.", + "subtitle": "Oldd fel a magasabb használati keretet, mind a 10 modellt és a beállítható érvelést.", "free": "Ingyenes", "pro": "Pro", "current": "Jelenlegi", @@ -1370,7 +1370,7 @@ "freeFeature3": "Minden vitafunkció", "freeFeature4": "3 napos ingyenes próba", "proFeature1": "Bővített MI felkészülési hozzáférés", - "proFeature2": "Mind a 11 modell, köztük a Claude Opus és Sonnet, a Muse Spark és a GLM Flash", + "proFeature2": "Mind a 10 modell, köztük a Claude Opus és Sonnet, a Muse Spark és a GLM Flash", "proFeature3": "Mélyreható kutatás Exa-val", "proFeature4": "Elsőbbségi feldolgozás", "proFeature5": "Minden vitafunkció", diff --git a/src/test/ai/streaming.test.ts b/src/test/ai/streaming.test.ts index 49b34ad04..ec0707804 100644 --- a/src/test/ai/streaming.test.ts +++ b/src/test/ai/streaming.test.ts @@ -88,38 +88,38 @@ describe('AI streaming helpers', () => { }); it('enables streaming for basic OpenRouter ask chats', () => { - expect(isAskStreamingSupported({ provider: 'openrouter', model: 'openai/gpt-5.6-luna' })).toBe(true); + expect(isAskStreamingSupported({ provider: 'openrouter', model: 'openai/gpt-6-luna' })).toBe(true); expect(isAskStreamingSupported({ provider: 'openai', model: 'gpt-5.4-mini' })).toBe(false); expect(isAskStreamingSupported({ provider: 'google', model: 'gemini-3-flash-preview' })).toBe(false); expect(isAskStreamingSupported({ provider: 'anthropic', model: 'claude-haiku-4-5-20251001' })).toBe(false); expect(isAskStreamingSupported({ provider: 'unknown' as string, model: 'x' })).toBe(false); expect(isAskStreamingSupported( - { provider: 'openrouter', model: 'openai/gpt-5.6-luna' }, + { provider: 'openrouter', model: 'openai/gpt-6-luna' }, { citationMode: true } )).toBe(false); expect(isAskStreamingSupported( - { provider: 'openrouter', model: 'openai/gpt-5.6-luna' }, + { provider: 'openrouter', model: 'openai/gpt-6-luna' }, { webSearch: true } )).toBe(false); expect(isAskStreamingSupported( - { provider: 'openrouter', model: 'openai/gpt-5.6-luna' }, + { provider: 'openrouter', model: 'openai/gpt-6-luna' }, { prosConsMode: true } )).toBe(false); expect(isAskStreamingSupported( - { provider: 'openrouter', model: 'openai/gpt-5.6-luna' }, + { provider: 'openrouter', model: 'openai/gpt-6-luna' }, { argumentMapMode: true } )).toBe(false); expect(isAskStreamingSupported( - { provider: 'openrouter', model: 'openai/gpt-5.6-luna' }, + { provider: 'openrouter', model: 'openai/gpt-6-luna' }, { mode: 'clarify' } )).toBe(false); expect(isAskStreamingSupported( - { provider: 'openrouter', model: 'openai/gpt-5.6-luna' }, + { provider: 'openrouter', model: 'openai/gpt-6-luna' }, { mode: 'general' } )).toBe(true); for (const mode of ['clarify', 'summarize', 'proscons', 'argumentmap']) { expect(isAskStreamingSupported( - { provider: 'openrouter', model: 'openai/gpt-5.6-luna' }, + { provider: 'openrouter', model: 'openai/gpt-6-luna' }, { mode } )).toBe(false); } diff --git a/src/test/debate/debate-context.test.tsx b/src/test/debate/debate-context.test.tsx index 64ac090eb..765f78ca0 100644 --- a/src/test/debate/debate-context.test.tsx +++ b/src/test/debate/debate-context.test.tsx @@ -128,7 +128,7 @@ describe('Current Debate context in requests and the Review control', () => { }); const brainstormInput = { motion: 'This House would make public transport free', side: 'pro' as const, outputMode: 'topfive' as const, language: 'en' }; - const config = { provider: 'openrouter' as const, model: 'openai/gpt-5.6-luna' }; + const config = { provider: 'openrouter' as const, model: 'openai/gpt-6-luna' }; const lastBody = () => (supabaseMock.functions.invoke.mock.calls.at(-1) as unknown as [string, { body: Record }])[1].body; it('sends IDs only, and a Review choice lasts for exactly one successful request', async () => { diff --git a/src/test/security-hardening.test.ts b/src/test/security-hardening.test.ts index 81a543966..c6a3c59b6 100644 --- a/src/test/security-hardening.test.ts +++ b/src/test/security-hardening.test.ts @@ -76,7 +76,7 @@ describe('security hardening regressions', () => { it('uses allowlisted model/provider pairs and SSRF-safe citation verification', () => { expect(modelCatalog).toContain('id: "z-ai/glm-5.3-flash"'); expect(modelCatalog).toContain('id: "anthropic/claude-haiku-5.5"'); - expect(modelCatalog).toContain('id: "openai/gpt-5.6-luna"'); + expect(modelCatalog).toContain('id: "openai/gpt-6-luna"'); expect(modelCatalog).toContain('id: "google/gemini-3.8-flash"'); expect(ai).toContain('function modelForRequest'); expect(stream).toContain('isModelSelection(rawModel)'); diff --git a/supabase/README.md b/supabase/README.md index f3ea3b4b2..ad6ae5242 100644 --- a/supabase/README.md +++ b/supabase/README.md @@ -36,7 +36,7 @@ supabase/tests/ pgTAP database and policy tests Both endpoints accept an optional `debateContext` of IDs (`debateId`, `side`, `sourceIds`, `outputs`). `_shared/debate-context-loader.ts` loads the matching material with the service role, filtering every query by the caller's user ID and the one debate, and returns 404 for a debate the caller does not own before any quota is reserved. Requests without it still accept the legacy `sourceIds` field. -Both endpoints route every model inference through OpenRouter and enforce the shared eleven-model Basic/Pro catalog. Claude Haiku 5.5 is the Basic default and Claude Opus 5.5 the Pro default; Pro-only models require active Pro access. Retired catalog IDs from older clients resolve to their successor before validation. +Both endpoints route every model inference through OpenRouter and enforce the shared ten-model Basic/Pro catalog. Claude Haiku 5.5 is the Basic default and Claude Opus 5.5 the Pro default; Pro-only models require active Pro access. Retired catalog IDs from older clients resolve to their successor before validation. The browser boundary is [`../src/lib/ai/client.ts`](../src/lib/ai/client.ts). Shared prompt, research, source-quality, cost, entitlement, CORS, and control logic lives in `_shared`. diff --git a/supabase/functions/_shared/auto-model-router.test.ts b/supabase/functions/_shared/auto-model-router.test.ts index 8d67abb47..e8c1bc5d5 100644 --- a/supabase/functions/_shared/auto-model-router.test.ts +++ b/supabase/functions/_shared/auto-model-router.test.ts @@ -48,9 +48,9 @@ Deno.test("Auto Fast can route logic-heavy work to DeepSeek Flash", async () => feature: "attack", input: { caseText: "A long causal chain requiring adversarial logical analysis" }, canAccessProModels: false, - callJevImpl: jevReturning(choiceResult("deepseek/deepseek-v4-flash-0731")), + callJevImpl: jevReturning(choiceResult("deepseek/deepseek-v4.1-flash")), }); - if (resolved.resolvedModel !== "deepseek/deepseek-v4-flash-0731" || resolved.usedFallback) { + if (resolved.resolvedModel !== "deepseek/deepseek-v4.1-flash" || resolved.usedFallback) { throw new Error(`logic-heavy route was not accepted: ${JSON.stringify(resolved)}`); } }); @@ -61,7 +61,7 @@ Deno.test("Auto Fast falls back to Claude Haiku when Jev is uncertain or unavail feature: "ask", input: { message: "Help me" }, canAccessProModels: false, - callJevImpl: jevReturning(choiceResult("openai/gpt-5.6-luna", 0.54)), + callJevImpl: jevReturning(choiceResult("openai/gpt-6-luna", 0.54)), }); if (uncertain.resolvedModel !== "anthropic/claude-haiku-5.5" || !uncertain.usedFallback) throw new Error("uncertain route did not fall back"); @@ -95,7 +95,7 @@ Deno.test("Auto Pro rejects Basic before calling Jev", async () => { feature: "generate", input: { motion: "Should cities ban cars?" }, canAccessProModels: false, - callJevImpl: jevReturning(choiceResult("openai/gpt-5.6-sol"), observed), + callJevImpl: jevReturning(choiceResult("openai/gpt-6.1-sol"), observed), }); } catch (error) { if (!(error instanceof Error) || !error.message.includes("Pro")) throw error; @@ -106,7 +106,7 @@ Deno.test("Auto Pro rejects Basic before calling Jev", async () => { }); Deno.test("Auto Pro accepts only its approved smart-model pool", async () => { - for (const model of ["anthropic/claude-opus-5.5", "anthropic/claude-sonnet-5.5", "openai/gpt-5.6-sol"]) { + for (const model of ["anthropic/claude-opus-5.5", "anthropic/claude-sonnet-5.5", "openai/gpt-6.1-sol"]) { const resolved = await resolveModelSelection({ requestedModel: AUTO_PRO, feature: "generate", diff --git a/supabase/functions/_shared/auto-model-router.ts b/supabase/functions/_shared/auto-model-router.ts index 46608e5bf..c41add434 100644 --- a/supabase/functions/_shared/auto-model-router.ts +++ b/supabase/functions/_shared/auto-model-router.ts @@ -20,15 +20,15 @@ const MAX_ROUTING_STATE_CHARS = 8_000; const FAST_CANDIDATES = { "anthropic/claude-haiku-5.5": "Default for ordinary DebateLab work: cases, rebuttals, coaching, and structured output.", "z-ai/glm-5.3-flash": "Best for efficient long-context requests that are mostly extraction or summarizing.", - "openai/gpt-5.6-luna": "Best for short latency-sensitive conversation and simple coaching.", + "openai/gpt-6-luna": "Best for short latency-sensitive conversation and simple coaching.", "google/gemini-3.8-flash": "Best for very long input, multilingual output, or broad context handling.", - "deepseek/deepseek-v4-flash-0731": "Best for logic-heavy analysis, adversarial attack, or structured reasoning at low cost.", + "deepseek/deepseek-v4.1-flash": "Best for logic-heavy analysis, adversarial attack, or structured reasoning at low cost.", } as const; const PRO_CANDIDATES = { "anthropic/claude-opus-5.5": "Default for professional DebateLab work: nuanced rhetoric, adversarial critique, and long-form writing.", "anthropic/claude-sonnet-5.5": "Best for quick, simpler Pro requests where speed matters more than depth.", - "openai/gpt-5.6-sol": "Best for complex structured reasoning and reliable multi-part analysis.", + "openai/gpt-6.1-sol": "Best for complex structured reasoning and reliable multi-part analysis.", } as const; type AutoCandidate = keyof typeof FAST_CANDIDATES | keyof typeof PRO_CANDIDATES; diff --git a/supabase/functions/_shared/entitlements.test.ts b/supabase/functions/_shared/entitlements.test.ts index cc4c79baf..72b117a18 100644 --- a/supabase/functions/_shared/entitlements.test.ts +++ b/supabase/functions/_shared/entitlements.test.ts @@ -104,7 +104,7 @@ Deno.test("banned users are refused before any quota is consumed", async () => { user_roles: { role: "banned" }, get quota() { quotaConsumed = true; return [{ allowed: true, current_count: 1 }]; }, }, async () => { - const result = await checkRateLimit("user-1", "openai/gpt-5.6-luna"); + const result = await checkRateLimit("user-1", "openai/gpt-6-luna"); assertEquals(result.allowed, false, "allowed"); assertEquals(result.code, "banned", "code"); assertEquals(result.statusCode, 403, "statusCode"); diff --git a/supabase/functions/_shared/model-catalog.test.ts b/supabase/functions/_shared/model-catalog.test.ts index 51242b727..e96d8b063 100644 --- a/supabase/functions/_shared/model-catalog.test.ts +++ b/supabase/functions/_shared/model-catalog.test.ts @@ -5,6 +5,7 @@ import { getModelCatalogEntry, isReasoningEffort, MODEL_CATALOG, + retiredModelSuccessor, } from "./model-catalog.ts"; Deno.test("every catalog model supports the forced trial effort and both defaults resolve to valid tiers", () => { @@ -20,9 +21,9 @@ Deno.test("every catalog model supports the forced trial effort and both default Deno.test("model catalog marks only the approved Pro entries as Pro-only", () => { const basic = new Set([ "anthropic/claude-haiku-5.5", - "openai/gpt-5.6-luna", + "openai/gpt-6-luna", "google/gemini-3.8-flash", - "deepseek/deepseek-v4-flash-0731", + "deepseek/deepseek-v4.1-flash", ]); for (const entry of MODEL_CATALOG) { if (entry.isProOnly === basic.has(entry.id)) throw new Error(`incorrect tier for ${entry.id}`); @@ -39,15 +40,27 @@ Deno.test("reasoning effort validator accepts only the product values", () => { }); Deno.test("trial and unsupported Pro reasoning resolve server-side", () => { - if (effectiveReasoningEffort("openai/gpt-5.6-luna", "high", false) !== "low") { + if (effectiveReasoningEffort("openai/gpt-6-luna", "high", false) !== "low") { throw new Error("trial request did not force low"); } if (effectiveReasoningEffort("anthropic/claude-opus-5.5", "medium", true) !== "medium") { throw new Error("native Pro medium was changed"); } - for (const model of ["deepseek/deepseek-v4-flash-0731", "deepseek/deepseek-v4-pro-0813", "z-ai/glm-5.3-flash"]) { + for (const model of ["deepseek/deepseek-v4.1-flash", "deepseek/deepseek-v4-pro-0813", "z-ai/glm-5.3-flash"]) { if (effectiveReasoningEffort(model, "medium", true) !== "low") { throw new Error(`unsupported Pro medium was not lowered for ${model}`); } } }); + +Deno.test("retired model IDs resolve to catalog models without crossing into Pro", () => { + const retiredBasic = ["openai/gpt-5.6-luna", "deepseek/deepseek-v4-flash-0731"]; + const retiredPro = ["anthropic/claude-sonnet-5", "anthropic/claude-opus-5", "openai/gpt-5.6-sol", "openai/gpt-5.6-terra"]; + for (const id of [...retiredBasic, ...retiredPro]) { + const successor = retiredModelSuccessor(id); + const entry = successor && getModelCatalogEntry(successor); + if (!entry) throw new Error(`retired ${id} has no catalog successor`); + if (retiredBasic.includes(id) && entry.isProOnly) throw new Error(`retired Basic ${id} resolves to Pro ${entry.id}`); + } + if (retiredModelSuccessor(DEFAULT_MODEL) !== undefined) throw new Error("a current model was treated as retired"); +}); diff --git a/supabase/functions/_shared/model-catalog.ts b/supabase/functions/_shared/model-catalog.ts index 8841e745f..769a651ca 100644 --- a/supabase/functions/_shared/model-catalog.ts +++ b/supabase/functions/_shared/model-catalog.ts @@ -16,13 +16,12 @@ export interface ModelCatalogEntry { export const MODEL_CATALOG = [ { id: "anthropic/claude-haiku-5.5", name: "Claude Haiku 5.5", maxTokens: 128_000, isProOnly: false, supportedEfforts: ["low", "medium", "high"] }, - { id: "openai/gpt-5.6-luna", name: "GPT-5.6 Luna", maxTokens: 128_000, isProOnly: false, supportedEfforts: ["low", "medium", "high"] }, + { id: "openai/gpt-6-luna", name: "GPT-6 Luna", maxTokens: 128_000, isProOnly: false, supportedEfforts: ["low", "medium", "high"] }, { id: "google/gemini-3.8-flash", name: "Gemini 3.8 Flash", maxTokens: 65_536, isProOnly: false, supportedEfforts: ["low", "medium", "high"] }, - { id: "deepseek/deepseek-v4-flash-0731", name: "DeepSeek V4 Flash", maxTokens: 131_072, isProOnly: false, supportedEfforts: ["low", "high"] }, + { id: "deepseek/deepseek-v4.1-flash", name: "DeepSeek V4.1 Flash", maxTokens: 943_718, isProOnly: false, supportedEfforts: ["low", "high"] }, { id: "anthropic/claude-opus-5.5", name: "Claude Opus 5.5", maxTokens: 128_000, isProOnly: true, supportedEfforts: ["low", "medium", "high"] }, { id: "anthropic/claude-sonnet-5.5", name: "Claude Sonnet 5.5", maxTokens: 128_000, isProOnly: true, supportedEfforts: ["low", "medium", "high"] }, - { id: "openai/gpt-5.6-sol", name: "GPT-5.6 Sol", maxTokens: 128_000, isProOnly: true, supportedEfforts: ["low", "medium", "high"] }, - { id: "openai/gpt-5.6-terra", name: "GPT-5.6 Terra", maxTokens: 128_000, isProOnly: true, supportedEfforts: ["low", "medium", "high"] }, + { id: "openai/gpt-6.1-sol", name: "GPT-6.1 Sol", maxTokens: 128_000, isProOnly: true, supportedEfforts: ["low", "medium", "high"] }, { id: "deepseek/deepseek-v4-pro-0813", name: "DeepSeek V4 Pro", maxTokens: 384_000, isProOnly: true, supportedEfforts: ["low", "high"] }, { id: "z-ai/glm-5.3-flash", name: "GLM-5.3 Flash", maxTokens: 131_072, isProOnly: true, supportedEfforts: ["low", "high"] }, { id: "meta/muse-spark-1.3", name: "Muse Spark 1.3", maxTokens: 943_718, isProOnly: true, supportedEfforts: ["low", "medium", "high"] }, @@ -43,10 +42,15 @@ export const DEFAULT_PRO_MODEL = "anthropic/claude-opus-5.5" as const; // Retired catalog IDs that older clients (an installed PWA, a saved setting) // may still send. Both the server and the settings store resolve them here. -// Sonnet 5 was the Pro default, so its users move to the new Pro default. +// The old defaults (GPT-5.6 Luna, Claude Sonnet 5) move to the new defaults; +// the rest move to their family's successor. A Basic ID never maps to Pro. const RETIRED_MODEL_SUCCESSORS = new Map([ + ["openai/gpt-5.6-luna", DEFAULT_MODEL], + ["deepseek/deepseek-v4-flash-0731", "deepseek/deepseek-v4.1-flash"], ["anthropic/claude-sonnet-5", DEFAULT_PRO_MODEL], ["anthropic/claude-opus-5", DEFAULT_PRO_MODEL], + ["openai/gpt-5.6-sol", "openai/gpt-6.1-sol"], + ["openai/gpt-5.6-terra", "openai/gpt-6.1-sol"], ]); const MODEL_BY_ID = new Map(MODEL_CATALOG.map((entry) => [entry.id, entry])); diff --git a/supabase/functions/ai-stream/auth_test.ts b/supabase/functions/ai-stream/auth_test.ts index f287cf7eb..759cd4435 100644 --- a/supabase/functions/ai-stream/auth_test.ts +++ b/supabase/functions/ai-stream/auth_test.ts @@ -21,7 +21,7 @@ const send = (authorization?: string) => "Content-Type": "application/json", ...(authorization === undefined ? {} : { Authorization: authorization }), }, - body: JSON.stringify({ feature: "ask", model: "openai/gpt-5.6-luna", input: { message: "Question", mode: "general" } }), + body: JSON.stringify({ feature: "ask", model: "openai/gpt-6-luna", input: { message: "Question", mode: "general" } }), }); Deno.test("ai-stream refuses missing, empty, non-Bearer and rejected tokens with the shared auth errors", async () => { diff --git a/supabase/functions/ai-stream/handler_test.ts b/supabase/functions/ai-stream/handler_test.ts index 38ce3eb94..a6256c7d0 100644 --- a/supabase/functions/ai-stream/handler_test.ts +++ b/supabase/functions/ai-stream/handler_test.ts @@ -108,9 +108,9 @@ Deno.test("streaming Ask forwards the shared prompt and returns provider SSE", a probabilities: { "anthropic/claude-haiku-5.5": 0.96, "z-ai/glm-5.3-flash": 0.01, - "openai/gpt-5.6-luna": 0.02, + "openai/gpt-6-luna": 0.02, "google/gemini-3.8-flash": 0.01, - "deepseek/deepseek-v4-flash-0731": 0.0, + "deepseek/deepseek-v4.1-flash": 0.0, }, }, }, @@ -265,7 +265,7 @@ Deno.test("streaming Ask rejects advanced modes and toggles before quota or prov }, body: JSON.stringify({ feature: "ask", - model: "openai/gpt-5.6-luna", + model: "openai/gpt-6-luna", input: { message: "Question", ...testCase.input }, }), }), diff --git a/supabase/functions/ai-stream/index_test.ts b/supabase/functions/ai-stream/index_test.ts index 5c4067417..59dbe5dad 100644 --- a/supabase/functions/ai-stream/index_test.ts +++ b/supabase/functions/ai-stream/index_test.ts @@ -30,7 +30,7 @@ function request(signal?: AbortSignal): Request { headers: { Authorization: "Bearer test-user-token", "Content-Type": "application/json" }, body: JSON.stringify({ feature: "ask", - model: "openai/gpt-5.6-luna", + model: "openai/gpt-6-luna", input: { message: "Hello", mode: "general" }, }), }); diff --git a/supabase/functions/ai/debate-context.test.ts b/supabase/functions/ai/debate-context.test.ts index db2804cf2..8d0d8c99e 100644 --- a/supabase/functions/ai/debate-context.test.ts +++ b/supabase/functions/ai/debate-context.test.ts @@ -126,7 +126,7 @@ function aiRequest(body: Record, path = "ai"): Request { return new Request(`http://127.0.0.1/functions/v1/${path}`, { method: "POST", headers: { Authorization: "Bearer test-user-token", "Content-Type": "application/json" }, - body: JSON.stringify({ model: "openai/gpt-5.6-luna", ...body }), + body: JSON.stringify({ model: "openai/gpt-6-luna", ...body }), }); } diff --git a/supabase/functions/ai/generate-honesty_test.ts b/supabase/functions/ai/generate-honesty_test.ts index 759aa374e..ddd064d5a 100644 --- a/supabase/functions/ai/generate-honesty_test.ts +++ b/supabase/functions/ai/generate-honesty_test.ts @@ -128,7 +128,7 @@ Deno.test("generate marks every contention when research has no citable sources" }, body: JSON.stringify({ feature: "generate", - model: "openai/gpt-5.6-luna", + model: "openai/gpt-6-luna", input: { motion: "Should schools teach media literacy?", side: "pro", @@ -177,7 +177,7 @@ Deno.test("generate marks every contention when research has no citable sources" }, body: JSON.stringify({ feature: "generate", - model: "openai/gpt-5.6-luna", + model: "openai/gpt-6-luna", input: { motion: "Should schools teach media literacy?", side: "pro", diff --git a/supabase/functions/ai/handler-research.test.ts b/supabase/functions/ai/handler-research.test.ts index b40d21ab6..a9c3bd2cb 100644 --- a/supabase/functions/ai/handler-research.test.ts +++ b/supabase/functions/ai/handler-research.test.ts @@ -65,7 +65,7 @@ Deno.test("AI handlers reject malformed controls before quota or provider work", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ feature: "ask", - model: "openai/gpt-5.6-luna", + model: "openai/gpt-6-luna", input: { message: "Question", mode: "\u200Bgeneral", citationMode: "false" }, }), })); @@ -94,7 +94,7 @@ Deno.test("AI handlers reject malformed controls before quota or provider work", const languageResponse = await handleRequest(new Request("http://127.0.0.1/functions/v1/ai", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ feature, model: "openai/gpt-5.6-luna", input }), + body: JSON.stringify({ feature, model: "openai/gpt-6-luna", input }), })); if (languageResponse.status !== 400) throw new Error(`${feature} invalid language returned ${languageResponse.status}`); if (!(await languageResponse.text()).includes(`${feature}.language`)) throw new Error(`${feature} invalid language was accepted`); @@ -109,7 +109,7 @@ Deno.test("AI handlers reject malformed controls before quota or provider work", }, body: JSON.stringify({ feature: "ask", - model: "openai/gpt-5.6-luna", + model: "openai/gpt-6-luna", input: { message: "Question", mode: "general", citationMode: "false" }, }), })); @@ -146,7 +146,7 @@ Deno.test("AI handler rejects missing, empty, and invalid bearer tokens with the }, body: JSON.stringify({ feature: "ask", - model: "openai/gpt-5.6-luna", + model: "openai/gpt-6-luna", input: { message: "Question", mode: "general" }, }), })); @@ -269,7 +269,7 @@ Deno.test("factoids handler carries Exa filters and consulted-source metadata ac }, body: JSON.stringify({ feature: "factoids", - model: "openai/gpt-5.6-luna", + model: "openai/gpt-6-luna", input: { claim: "solar energy costs", direction: "for", @@ -379,7 +379,7 @@ Deno.test("Ask marks only surviving URL and research context as available", asyn const response = await handleRequest(new Request("http://127.0.0.1/functions/v1/ai", { method: "POST", headers: { Authorization: "Bearer test-user-token", "Content-Type": "application/json" }, - body: JSON.stringify({ feature: "ask", model: "openai/gpt-5.6-luna", input }), + body: JSON.stringify({ feature: "ask", model: "openai/gpt-6-luna", input }), })); if (response.status !== 200) throw new Error(`Ask handler returned ${response.status}: ${await response.text()}`); const mainRequest = providerBodies.find((body) => { @@ -512,7 +512,7 @@ Deno.test("AI handler ranks stable library sources and returns bounded assessmen headers: { Authorization: "Bearer test-user-token", "Content-Type": "application/json" }, body: JSON.stringify({ feature: "ask", - model: "openai/gpt-5.6-luna", + model: "openai/gpt-6-luna", input: { message: "Compare the evidence", mode: "general", webSearch: false }, sourceIds: [sourceA, sourceB], }), @@ -575,7 +575,7 @@ Deno.test("Crossfire returns newest-exchange signals without making them a reply headers: { Authorization: "Bearer test-user-token", "Content-Type": "application/json" }, body: JSON.stringify({ feature: "crossfire", - model: "openai/gpt-5.6-luna", + model: "openai/gpt-6-luna", input: { operation: "next-turn", config: config(mode), @@ -587,7 +587,7 @@ Deno.test("Crossfire returns newest-exchange signals without making them a reply const requestSignal = (mode: "question-ai" | "ai-questions-user", transcript: unknown[], reply: { type: "question" | "answer"; text: string }, signalToken: string) => handleRequest(new Request("http://127.0.0.1/functions/v1/ai", { method: "POST", headers: { Authorization: "Bearer test-user-token", "Content-Type": "application/json" }, - body: JSON.stringify({ feature: "crossfire", model: "openai/gpt-5.6-luna", input: { operation: "signal", config: config(mode), transcript, reply, signalToken } }), + body: JSON.stringify({ feature: "crossfire", model: "openai/gpt-6-luna", input: { operation: "signal", config: config(mode), transcript, reply, signalToken } }), })); try { @@ -692,7 +692,7 @@ Deno.test("AI handlers use strict OpenRouter routing and server-side reasoning t const response = await requestForModel("anthropic/claude-sonnet-5.5", "pro", effort); if (response.status !== 200) throw new Error(`Pro ${effort} returned ${response.status}: ${await response.text()}`); } - for (const model of ["deepseek/deepseek-v4-flash-0731", "deepseek/deepseek-v4-pro-0813", "z-ai/glm-5.3-flash"]) { + for (const model of ["deepseek/deepseek-v4.1-flash", "deepseek/deepseek-v4-pro-0813", "z-ai/glm-5.3-flash"]) { const response = await requestForModel(model, "pro", "medium"); if (response.status !== 200) throw new Error(`Pro medium ${model} returned ${response.status}: ${await response.text()}`); } @@ -786,7 +786,7 @@ Deno.test("streaming OpenRouter usage records zero and missing costs distinctly" const withZero = await handleStreamRequest(new Request("http://127.0.0.1/functions/v1/ai-stream", { method: "POST", headers: { Authorization: "Bearer test-user-token", "Content-Type": "application/json" }, - body: JSON.stringify({ feature: "ask", model: "openai/gpt-5.6-luna", input: { message: "Hello", mode: "general" } }), + body: JSON.stringify({ feature: "ask", model: "openai/gpt-6-luna", input: { message: "Hello", mode: "general" } }), })); await withZero.text(); if (streamBodies[0]?.max_tokens !== MAX_REQUEST_OUTPUT_TOKENS) throw new Error("stream output cap was not enforced"); @@ -796,7 +796,7 @@ Deno.test("streaming OpenRouter usage records zero and missing costs distinctly" const withoutUsage = await handleStreamRequest(new Request("http://127.0.0.1/functions/v1/ai-stream", { method: "POST", headers: { Authorization: "Bearer test-user-token", "Content-Type": "application/json" }, - body: JSON.stringify({ feature: "ask", model: "openai/gpt-5.6-luna", input: { message: "Hello", mode: "general" } }), + body: JSON.stringify({ feature: "ask", model: "openai/gpt-6-luna", input: { message: "Hello", mode: "general" } }), })); await withoutUsage.text(); if (usageRows.at(-1)?.cost_usd !== null) throw new Error(`missing stream cost was estimated: ${JSON.stringify(usageRows.at(-1))}`); @@ -827,7 +827,7 @@ Deno.test("streaming rejects provider errors and incomplete EOFs, while acceptin const request = () => new Request("http://127.0.0.1/functions/v1/ai-stream", { method: "POST", headers: { Authorization: "Bearer test-user-token", "Content-Type": "application/json" }, - body: JSON.stringify({ feature: "ask", model: "openai/gpt-5.6-luna", input: { message: "Hello", mode: "general" } }), + body: JSON.stringify({ feature: "ask", model: "openai/gpt-6-luna", input: { message: "Hello", mode: "general" } }), }); const testState = globalThis as typeof globalThis & { __testTier?: string }; @@ -896,7 +896,7 @@ Deno.test("cancelled JSON repair keeps observed tokens but makes the total cost const response = await handleRequest(new Request("http://127.0.0.1/functions/v1/ai", { method: "POST", headers: { Authorization: "Bearer test-user-token", "Content-Type": "application/json" }, - body: JSON.stringify({ feature: "ask", model: "openai/gpt-5.6-luna", input: { message: "Hello", mode: "general" } }), + body: JSON.stringify({ feature: "ask", model: "openai/gpt-6-luna", input: { message: "Hello", mode: "general" } }), signal: caller.signal, })); if (response.status !== 500) throw new Error(`cancelled repair returned ${response.status}`); @@ -958,7 +958,7 @@ Deno.test("stream terminal events stop an open upstream promptly", async () => { const request = (signal?: AbortSignal) => new Request("http://127.0.0.1/functions/v1/ai-stream", { method: "POST", headers: { Authorization: "Bearer test-user-token", "Content-Type": "application/json" }, - body: JSON.stringify({ feature: "ask", model: "openai/gpt-5.6-luna", input: { message: "Hello", mode: "general" } }), + body: JSON.stringify({ feature: "ask", model: "openai/gpt-6-luna", input: { message: "Hello", mode: "general" } }), signal, }); const withTimeout = async (promise: Promise, label: string): Promise => await Promise.race([ diff --git a/supabase/functions/ai/voice.test.ts b/supabase/functions/ai/voice.test.ts index af631f2db..268a2f9d5 100644 --- a/supabase/functions/ai/voice.test.ts +++ b/supabase/functions/ai/voice.test.ts @@ -75,7 +75,7 @@ function aiRequest(body: Record): Request { return new Request("http://127.0.0.1/functions/v1/ai", { method: "POST", headers: { Authorization: "Bearer test-user-token", "Content-Type": "application/json" }, - body: JSON.stringify({ model: "openai/gpt-5.6-luna", ...body }), + body: JSON.stringify({ model: "openai/gpt-6-luna", ...body }), }); } From 30e9ccf2cca1322ac3f92b0dbf772eeb4cda09c0 Mon Sep 17 00:00:00 2001 From: Adam Date: Fri, 9 Oct 2026 17:15:18 +0000 Subject: [PATCH 4/7] Read the requested model in the model-defaults recipe Live verify rewrites every upstream call to the Muse Spark pin, so the provider call's model is the pin, not Opus 5.5. Co-Authored-By: Claude Opus 5.5 --- .claude/skills/verify-debatelab/recipes/model-defaults.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.claude/skills/verify-debatelab/recipes/model-defaults.spec.ts b/.claude/skills/verify-debatelab/recipes/model-defaults.spec.ts index d4e3f5c5c..2ad83c52a 100644 --- a/.claude/skills/verify-debatelab/recipes/model-defaults.spec.ts +++ b/.claude/skills/verify-debatelab/recipes/model-defaults.spec.ts @@ -42,7 +42,8 @@ test("model-pro-default: a new Pro account starts on Claude Opus 5.5 and generat const body = await generateOnce(page); expect((body.meta as { resolvedModel?: string } | undefined)?.resolvedModel ?? OPUS).toBe(OPUS); const calls = await providerCalls(); - expect(calls.filter((call) => call.feature === "generate").map((call) => call.model)).toContain(OPUS); + // Live mode rewrites the upstream model to the pin, so read what the app asked for. + expect(calls.filter((call) => call.feature === "generate").map((call) => call.requestedModel ?? call.model)).toContain(OPUS); const { data: usage } = await admin.from("ai_requests").select("feature,status,model").eq("user_id", userId); expect(usage).toContainEqual(expect.objectContaining({ feature: "generate", status: "success", model: OPUS })); await evidence.json("02-ai-requests", usage); From d0894e37ad7f75f9a4b4762db606c11bef13ee6d Mon Sep 17 00:00:00 2001 From: Adam Date: Fri, 9 Oct 2026 17:24:05 +0000 Subject: [PATCH 5/7] Limit Basic to Haiku, Luna and Muse Spark contributor Basic accounts now get Claude Haiku 5.5, GPT-6 Luna and Muse Spark 1.3 on its contributor tier (meta/muse-spark-1.3-contributor), which replaces the regular Muse Spark 1.3. Gemini 3.8 Flash and DeepSeek V4.1 Flash move to Pro. Auto Fast routes only among the three Basic models. The retired DeepSeek V4 Flash was Basic, so it now resolves to the Basic default instead of the Pro-only V4.1 Flash. Retired Muse Spark 1.3 resolves to the contributor tier. Co-Authored-By: Claude Opus 5.5 --- .../verify-debatelab/features/model-defaults.md | 2 +- .../recipes/model-defaults.spec.ts | 7 +++++-- docs/architecture.md | 2 +- src/locales/en.json | 6 +++--- src/locales/es.json | 6 +++--- src/locales/hu.json | 6 +++--- supabase/README.md | 2 +- .../functions/_shared/auto-model-router.test.ts | 16 ++++++++-------- supabase/functions/_shared/auto-model-router.ts | 4 +--- supabase/functions/_shared/model-catalog.test.ts | 5 ++--- supabase/functions/_shared/model-catalog.ts | 12 +++++++----- supabase/functions/ai-stream/handler_test.ts | 4 +--- 12 files changed, 36 insertions(+), 36 deletions(-) diff --git a/.claude/skills/verify-debatelab/features/model-defaults.md b/.claude/skills/verify-debatelab/features/model-defaults.md index 80d98d955..00ab98457 100644 --- a/.claude/skills/verify-debatelab/features/model-defaults.md +++ b/.claude/skills/verify-debatelab/features/model-defaults.md @@ -1,6 +1,6 @@ # Model defaults and the model selector -Every AI tool has a model selector. A new Pro account starts every tool on Claude Opus 5.5, and a Basic (trial) account on Claude Haiku 5.5. Pro-only models show a `Pro` badge and are disabled for Basic accounts. A setting saved with a retired catalog ID is rewritten on the next load: GPT-5.6 Luna moves to Claude Haiku 5.5, Claude Sonnet 5 and Opus 5 to Claude Opus 5.5, GPT-5.6 Sol and Terra to GPT-6.1 Sol, and DeepSeek V4 Flash to DeepSeek V4.1 Flash. The Edge Functions resolve a retired ID from an older client the same way. A retired Basic ID never resolves to a Pro model. The catalog and defaults live in `supabase/functions/_shared/model-catalog.ts`. +Every AI tool has a model selector. Basic accounts can pick Claude Haiku 5.5, GPT-6 Luna, or Muse Spark 1.3 (`meta/muse-spark-1.3-contributor`); everything else is Pro. A new Pro account starts every tool on Claude Opus 5.5, and a Basic (trial) account on Claude Haiku 5.5. Pro-only models show a `Pro` badge and are disabled for Basic accounts. A setting saved with a retired catalog ID is rewritten on the next load: GPT-5.6 Luna moves to Claude Haiku 5.5, Claude Sonnet 5 and Opus 5 to Claude Opus 5.5, GPT-5.6 Sol and Terra to GPT-6.1 Sol, DeepSeek V4 Flash to Claude Haiku 5.5, and Muse Spark 1.3 to its contributor tier. The Edge Functions resolve a retired ID from an older client the same way. A retired Basic ID never resolves to a Pro model. The catalog and defaults live in `supabase/functions/_shared/model-catalog.ts`. ## Sub-features diff --git a/.claude/skills/verify-debatelab/recipes/model-defaults.spec.ts b/.claude/skills/verify-debatelab/recipes/model-defaults.spec.ts index 2ad83c52a..07d11abf3 100644 --- a/.claude/skills/verify-debatelab/recipes/model-defaults.spec.ts +++ b/.claude/skills/verify-debatelab/recipes/model-defaults.spec.ts @@ -50,7 +50,7 @@ test("model-pro-default: a new Pro account starts on Claude Opus 5.5 and generat await evidence.shot("02-generated-with-opus"); }); -test("model-basic-default: a Basic account starts on Claude Haiku 5.5 and Pro models stay locked", async ({ signedIn: page, user, evidence }) => { +test("model-basic-default: a Basic account starts on Claude Haiku 5.5 with only Haiku, Luna and Muse Spark unlocked", async ({ signedIn: page, user, evidence }) => { const userId = await user.userId(); // Demo sign-in grants Pro; turn the account into a fresh trial with nothing saved. await updateSettings(userId, { @@ -68,9 +68,12 @@ test("model-basic-default: a Basic account starts on Claude Haiku 5.5 and Pro mo await selector.click(); await expect(page.getByRole("option", { name: /Claude Opus 5\.5/ })).toBeDisabled(); await expect(page.getByRole("option", { name: /Claude Sonnet 5\.5/ })).toBeDisabled(); + await expect(page.getByRole("option", { name: /Gemini 3\.8 Flash/ })).toBeDisabled(); + await expect(page.getByRole("option", { name: /GPT-6 Luna/ })).toBeEnabled(); + await expect(page.getByRole("option", { name: /Muse Spark 1\.3/ })).toBeEnabled(); await evidence.shot("01-basic-model-list"); await page.keyboard.press("Escape"); - await evidence.step("Basic account: selector shows Claude Haiku 5.5, Opus and Sonnet 5.5 locked behind Pro"); + await evidence.step("Basic account: selector shows Claude Haiku 5.5; Luna and Muse Spark enabled; Opus, Sonnet and Gemini locked behind Pro"); await generateOnce(page); const { data: usage } = await admin.from("ai_requests").select("feature,status,model").eq("user_id", userId); diff --git a/docs/architecture.md b/docs/architecture.md index 6348242a4..840ab4028 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -130,7 +130,7 @@ Checkout and portal functions create Stripe sessions after authenticated ownersh All model requests use the single OpenRouter backend route and `OPENROUTER_API_KEY`. The shared ten-model catalog, its defaults (Claude Haiku 5.5 for Basic, Claude Opus 5.5 for Pro), Pro-only entries, retired-ID successors, and request controls live in [`supabase/functions/_shared/model-catalog.ts`](../supabase/functions/_shared/model-catalog.ts). OpenRouter usage remains raw in the audit row, and the reported charge is unknown when the provider does not return a complete cost. The opponent's voice is the one exception to the OpenRouter route: the `voice` function calls ElevenLabs with `ELEVENLABS_API_KEY`. ElevenLabs reports billed characters, not dollars, so [`supabase/functions/_shared/cost.ts`](../supabase/functions/_shared/cost.ts) prices those rows from a dated list-price snapshot; promotions and plan credits are not modelled, so a voice row can overstate the charge. -`Auto Fast` and `Auto Pro` are virtual selections, not provider model IDs. The Edge Functions resolve them through TypeSafe Jev on OpenRouter before calling the concrete writing model. Auto Fast is available to Basic accounts and normally resolves to Claude Haiku 5.5; it can also pick GLM 5.3 Flash, whose manual selection keeps its Pro restriction. Auto Pro remains Pro-only and normally resolves to Claude Opus 5.5. Provider calls, usage rows, and response metadata record the concrete resolved model, while response metadata also retains the requested automatic selection. A Jev router call uses the same OpenRouter key and does not consume a second daily quota unit. +`Auto Fast` and `Auto Pro` are virtual selections, not provider model IDs. The Edge Functions resolve them through TypeSafe Jev on OpenRouter before calling the concrete writing model. Auto Fast is available to Basic accounts, routes only among the Basic models (Claude Haiku 5.5, GPT-6 Luna, Muse Spark 1.3 Contributor), and normally resolves to Claude Haiku 5.5. Auto Pro remains Pro-only and normally resolves to Claude Opus 5.5. Provider calls, usage rows, and response metadata record the concrete resolved model, while response metadata also retains the requested automatic selection. A Jev router call uses the same OpenRouter key and does not consume a second daily quota unit. ## Observability diff --git a/src/locales/en.json b/src/locales/en.json index a987ef41e..a38fa34c7 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -932,7 +932,7 @@ "trialFeature4": "3-day access", "proFeature1": "Expanded AI prep access", "proFeature2": "All 11 prep modes", - "proFeature3": "All 10 models, including Claude Opus and Sonnet, Muse Spark, and GLM Flash", + "proFeature3": "All 10 models, including Claude Opus and Sonnet, GPT-6.1 Sol, and Gemini", "proFeature4": "Priority support", "proFeature5": "Active Grounding™ research", "startTrial": "Start Free Trial", @@ -948,7 +948,7 @@ "q1": "How much does DebateLab cost?", "a1": "DebateLab offers a 3-day free trial. After that, Pro is $5/month + tax for expanded access and all 10 models.", "q2": "What do I get with Pro?", - "a2": "Expanded AI prep access, all 10 models including Claude Opus and Sonnet, Muse Spark, and GLM Flash, Active Grounding™ research, and priority support.", + "a2": "Expanded AI prep access, all 10 models including Claude Opus and Sonnet, GPT-6.1 Sol, and Gemini, Active Grounding™ research, and priority support.", "q3": "Can I cancel my subscription?", "a3": "Yes, you can cancel anytime from your Settings page. You'll retain access until the end of your billing period.", "q4": "Does the AI make things up?", @@ -1370,7 +1370,7 @@ "freeFeature3": "All debate features", "freeFeature4": "3-day free trial", "proFeature1": "Expanded AI prep access", - "proFeature2": "All 10 models, including Claude Opus and Sonnet, Muse Spark, and GLM Flash", + "proFeature2": "All 10 models, including Claude Opus and Sonnet, GPT-6.1 Sol, and Gemini", "proFeature3": "Deep research with Exa", "proFeature4": "Priority processing", "proFeature5": "All debate features", diff --git a/src/locales/es.json b/src/locales/es.json index 3eafbc716..c7cab3d13 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -932,7 +932,7 @@ "trialFeature4": "Acceso por 3 días", "proFeature1": "Acceso ampliado a preparación con IA", "proFeature2": "Las 11 herramientas de preparación", - "proFeature3": "Los 10 modelos, incluidos Claude Opus y Sonnet, Muse Spark y GLM Flash", + "proFeature3": "Los 10 modelos, incluidos Claude Opus y Sonnet, GPT-6.1 Sol y Gemini", "proFeature4": "Soporte prioritario", "proFeature5": "Investigación Active Grounding™", "startTrial": "Comenzar Prueba Gratis", @@ -948,7 +948,7 @@ "q1": "¿Cuánto cuesta DebateLab?", "a1": "DebateLab ofrece una prueba gratis de 3 días. Después, Pro cuesta $5/mes + impuestos para acceso ampliado y los 10 modelos.", "q2": "¿Qué obtengo con Pro?", - "a2": "Acceso ampliado a preparación con IA, los 10 modelos incluidos Claude Opus y Sonnet, Muse Spark y GLM Flash, investigación Active Grounding™ y soporte prioritario.", + "a2": "Acceso ampliado a preparación con IA, los 10 modelos incluidos Claude Opus y Sonnet, GPT-6.1 Sol y Gemini, investigación Active Grounding™ y soporte prioritario.", "q3": "¿Puedo cancelar mi suscripción?", "a3": "Sí, puedes cancelar en cualquier momento desde tu página de Ajustes. Mantendrás el acceso hasta el final de tu período de facturación.", "q4": "¿La IA inventa cosas?", @@ -1370,7 +1370,7 @@ "freeFeature3": "Todas las funciones de debate", "freeFeature4": "Prueba gratis de 3 días", "proFeature1": "Acceso ampliado a preparación con IA", - "proFeature2": "Los 10 modelos, incluidos Claude Opus y Sonnet, Muse Spark y GLM Flash", + "proFeature2": "Los 10 modelos, incluidos Claude Opus y Sonnet, GPT-6.1 Sol y Gemini", "proFeature3": "Investigación profunda con Exa", "proFeature4": "Procesamiento prioritario", "proFeature5": "Todas las funciones de debate", diff --git a/src/locales/hu.json b/src/locales/hu.json index 5ab77703f..baec19ca5 100644 --- a/src/locales/hu.json +++ b/src/locales/hu.json @@ -932,7 +932,7 @@ "trialFeature4": "3 napos hozzáférés", "proFeature1": "Bővített MI felkészülési hozzáférés", "proFeature2": "Mind a 11 felkészülési eszköz", - "proFeature3": "Mind a 10 modell, köztük a Claude Opus és Sonnet, a Muse Spark és a GLM Flash", + "proFeature3": "Mind a 10 modell, köztük a Claude Opus és Sonnet, a GPT-6.1 Sol és a Gemini", "proFeature4": "Elsőbbségi támogatás", "proFeature5": "Active Grounding™ kutatás", "startTrial": "Ingyenes Próba Indítása", @@ -948,7 +948,7 @@ "q1": "Mennyibe kerül a DebateLab?", "a1": "A DebateLab 3 napos ingyenes próbát kínál. Ezután a Pro $5/hó + adó bővített hozzáférést és mind a 10 modellt kínálja.", "q2": "Mit kapok a Pro-val?", - "a2": "Bővített MI felkészülési hozzáférés mind a 10 modellel, köztük a Claude Opus és Sonnet, a Muse Spark és a GLM Flash modellekkel, Active Grounding™ kutatással és elsőbbségi támogatással.", + "a2": "Bővített MI felkészülési hozzáférés mind a 10 modellel, köztük a Claude Opus és Sonnet, a GPT-6.1 Sol és a Gemini modellekkel, Active Grounding™ kutatással és elsőbbségi támogatással.", "q3": "Lemondhatom az előfizetésem?", "a3": "Igen, bármikor lemondhatod a Beállítások oldalon. A számlázási időszak végéig megtartod a hozzáférést.", "q4": "Az MI kitalál dolgokat?", @@ -1370,7 +1370,7 @@ "freeFeature3": "Minden vitafunkció", "freeFeature4": "3 napos ingyenes próba", "proFeature1": "Bővített MI felkészülési hozzáférés", - "proFeature2": "Mind a 10 modell, köztük a Claude Opus és Sonnet, a Muse Spark és a GLM Flash", + "proFeature2": "Mind a 10 modell, köztük a Claude Opus és Sonnet, a GPT-6.1 Sol és a Gemini", "proFeature3": "Mélyreható kutatás Exa-val", "proFeature4": "Elsőbbségi feldolgozás", "proFeature5": "Minden vitafunkció", diff --git a/supabase/README.md b/supabase/README.md index ad6ae5242..4ad607716 100644 --- a/supabase/README.md +++ b/supabase/README.md @@ -36,7 +36,7 @@ supabase/tests/ pgTAP database and policy tests Both endpoints accept an optional `debateContext` of IDs (`debateId`, `side`, `sourceIds`, `outputs`). `_shared/debate-context-loader.ts` loads the matching material with the service role, filtering every query by the caller's user ID and the one debate, and returns 404 for a debate the caller does not own before any quota is reserved. Requests without it still accept the legacy `sourceIds` field. -Both endpoints route every model inference through OpenRouter and enforce the shared ten-model Basic/Pro catalog. Claude Haiku 5.5 is the Basic default and Claude Opus 5.5 the Pro default; Pro-only models require active Pro access. Retired catalog IDs from older clients resolve to their successor before validation. +Both endpoints route every model inference through OpenRouter and enforce the shared ten-model Basic/Pro catalog. Basic accounts get Claude Haiku 5.5 (the default), GPT-6 Luna, and Muse Spark 1.3 Contributor; Claude Opus 5.5 is the Pro default; Pro-only models require active Pro access. Retired catalog IDs from older clients resolve to their successor before validation. The browser boundary is [`../src/lib/ai/client.ts`](../src/lib/ai/client.ts). Shared prompt, research, source-quality, cost, entitlement, CORS, and control logic lives in `_shared`. diff --git a/supabase/functions/_shared/auto-model-router.test.ts b/supabase/functions/_shared/auto-model-router.test.ts index e8c1bc5d5..a6cb9fbf5 100644 --- a/supabase/functions/_shared/auto-model-router.test.ts +++ b/supabase/functions/_shared/auto-model-router.test.ts @@ -42,16 +42,16 @@ Deno.test("Auto Fast normally resolves to Claude Haiku for a Basic user", async } }); -Deno.test("Auto Fast can route logic-heavy work to DeepSeek Flash", async () => { +Deno.test("Auto Fast can route long-context work to Muse Spark", async () => { const resolved = await resolveModelSelection({ requestedModel: AUTO_FAST, feature: "attack", - input: { caseText: "A long causal chain requiring adversarial logical analysis" }, + input: { caseText: "A very long multilingual case file requiring broad context handling" }, canAccessProModels: false, - callJevImpl: jevReturning(choiceResult("deepseek/deepseek-v4.1-flash")), + callJevImpl: jevReturning(choiceResult("meta/muse-spark-1.3-contributor")), }); - if (resolved.resolvedModel !== "deepseek/deepseek-v4.1-flash" || resolved.usedFallback) { - throw new Error(`logic-heavy route was not accepted: ${JSON.stringify(resolved)}`); + if (resolved.resolvedModel !== "meta/muse-spark-1.3-contributor" || resolved.usedFallback) { + throw new Error(`long-context route was not accepted: ${JSON.stringify(resolved)}`); } }); @@ -122,7 +122,7 @@ Deno.test("Auto Pro accepts only its approved smart-model pool", async () => { feature: "generate", input: { motion: "Complex international law motion" }, canAccessProModels: true, - callJevImpl: jevReturning(choiceResult("meta/muse-spark-1.3")), + callJevImpl: jevReturning(choiceResult("z-ai/glm-5.3-flash")), }); if (outsidePool.resolvedModel !== "anthropic/claude-opus-5.5" || !outsidePool.usedFallback) { throw new Error("Auto Pro accepted a model outside its pool"); @@ -132,13 +132,13 @@ Deno.test("Auto Pro accepts only its approved smart-model pool", async () => { Deno.test("manual model selections bypass Jev", async () => { const observed = { calls: 0 }; const resolved = await resolveModelSelection({ - requestedModel: "google/gemini-3.8-flash", + requestedModel: "openai/gpt-6-luna", feature: "ask", input: { message: "Hi" }, canAccessProModels: false, callJevImpl: jevReturning(choiceResult("z-ai/glm-5.3-flash"), observed), }); - if (resolved.resolvedModel !== "google/gemini-3.8-flash" || resolved.requestedModel !== "google/gemini-3.8-flash") { + if (resolved.resolvedModel !== "openai/gpt-6-luna" || resolved.requestedModel !== "openai/gpt-6-luna") { throw new Error("manual selection changed"); } if (observed.calls !== 0) throw new Error("manual selection called Jev"); diff --git a/supabase/functions/_shared/auto-model-router.ts b/supabase/functions/_shared/auto-model-router.ts index c41add434..a16d465fd 100644 --- a/supabase/functions/_shared/auto-model-router.ts +++ b/supabase/functions/_shared/auto-model-router.ts @@ -19,10 +19,8 @@ const MAX_ROUTING_STATE_CHARS = 8_000; const FAST_CANDIDATES = { "anthropic/claude-haiku-5.5": "Default for ordinary DebateLab work: cases, rebuttals, coaching, and structured output.", - "z-ai/glm-5.3-flash": "Best for efficient long-context requests that are mostly extraction or summarizing.", "openai/gpt-6-luna": "Best for short latency-sensitive conversation and simple coaching.", - "google/gemini-3.8-flash": "Best for very long input, multilingual output, or broad context handling.", - "deepseek/deepseek-v4.1-flash": "Best for logic-heavy analysis, adversarial attack, or structured reasoning at low cost.", + "meta/muse-spark-1.3-contributor": "Best for very long input, multilingual output, or broad context handling.", } as const; const PRO_CANDIDATES = { diff --git a/supabase/functions/_shared/model-catalog.test.ts b/supabase/functions/_shared/model-catalog.test.ts index e96d8b063..141954c13 100644 --- a/supabase/functions/_shared/model-catalog.test.ts +++ b/supabase/functions/_shared/model-catalog.test.ts @@ -22,8 +22,7 @@ Deno.test("model catalog marks only the approved Pro entries as Pro-only", () => const basic = new Set([ "anthropic/claude-haiku-5.5", "openai/gpt-6-luna", - "google/gemini-3.8-flash", - "deepseek/deepseek-v4.1-flash", + "meta/muse-spark-1.3-contributor", ]); for (const entry of MODEL_CATALOG) { if (entry.isProOnly === basic.has(entry.id)) throw new Error(`incorrect tier for ${entry.id}`); @@ -55,7 +54,7 @@ Deno.test("trial and unsupported Pro reasoning resolve server-side", () => { Deno.test("retired model IDs resolve to catalog models without crossing into Pro", () => { const retiredBasic = ["openai/gpt-5.6-luna", "deepseek/deepseek-v4-flash-0731"]; - const retiredPro = ["anthropic/claude-sonnet-5", "anthropic/claude-opus-5", "openai/gpt-5.6-sol", "openai/gpt-5.6-terra"]; + const retiredPro = ["anthropic/claude-sonnet-5", "anthropic/claude-opus-5", "openai/gpt-5.6-sol", "openai/gpt-5.6-terra", "meta/muse-spark-1.3"]; for (const id of [...retiredBasic, ...retiredPro]) { const successor = retiredModelSuccessor(id); const entry = successor && getModelCatalogEntry(successor); diff --git a/supabase/functions/_shared/model-catalog.ts b/supabase/functions/_shared/model-catalog.ts index 769a651ca..9ac386965 100644 --- a/supabase/functions/_shared/model-catalog.ts +++ b/supabase/functions/_shared/model-catalog.ts @@ -17,14 +17,14 @@ export interface ModelCatalogEntry { export const MODEL_CATALOG = [ { id: "anthropic/claude-haiku-5.5", name: "Claude Haiku 5.5", maxTokens: 128_000, isProOnly: false, supportedEfforts: ["low", "medium", "high"] }, { id: "openai/gpt-6-luna", name: "GPT-6 Luna", maxTokens: 128_000, isProOnly: false, supportedEfforts: ["low", "medium", "high"] }, - { id: "google/gemini-3.8-flash", name: "Gemini 3.8 Flash", maxTokens: 65_536, isProOnly: false, supportedEfforts: ["low", "medium", "high"] }, - { id: "deepseek/deepseek-v4.1-flash", name: "DeepSeek V4.1 Flash", maxTokens: 943_718, isProOnly: false, supportedEfforts: ["low", "high"] }, + { id: "meta/muse-spark-1.3-contributor", name: "Muse Spark 1.3", maxTokens: 943_718, isProOnly: false, supportedEfforts: ["low", "medium", "high"] }, { id: "anthropic/claude-opus-5.5", name: "Claude Opus 5.5", maxTokens: 128_000, isProOnly: true, supportedEfforts: ["low", "medium", "high"] }, { id: "anthropic/claude-sonnet-5.5", name: "Claude Sonnet 5.5", maxTokens: 128_000, isProOnly: true, supportedEfforts: ["low", "medium", "high"] }, { id: "openai/gpt-6.1-sol", name: "GPT-6.1 Sol", maxTokens: 128_000, isProOnly: true, supportedEfforts: ["low", "medium", "high"] }, + { id: "google/gemini-3.8-flash", name: "Gemini 3.8 Flash", maxTokens: 65_536, isProOnly: true, supportedEfforts: ["low", "medium", "high"] }, + { id: "deepseek/deepseek-v4.1-flash", name: "DeepSeek V4.1 Flash", maxTokens: 943_718, isProOnly: true, supportedEfforts: ["low", "high"] }, { id: "deepseek/deepseek-v4-pro-0813", name: "DeepSeek V4 Pro", maxTokens: 384_000, isProOnly: true, supportedEfforts: ["low", "high"] }, { id: "z-ai/glm-5.3-flash", name: "GLM-5.3 Flash", maxTokens: 131_072, isProOnly: true, supportedEfforts: ["low", "high"] }, - { id: "meta/muse-spark-1.3", name: "Muse Spark 1.3", maxTokens: 943_718, isProOnly: true, supportedEfforts: ["low", "medium", "high"] }, ] as const satisfies readonly ModelCatalogEntry[]; export type ModelId = (typeof MODEL_CATALOG)[number]["id"]; @@ -43,14 +43,16 @@ export const DEFAULT_PRO_MODEL = "anthropic/claude-opus-5.5" as const; // Retired catalog IDs that older clients (an installed PWA, a saved setting) // may still send. Both the server and the settings store resolve them here. // The old defaults (GPT-5.6 Luna, Claude Sonnet 5) move to the new defaults; -// the rest move to their family's successor. A Basic ID never maps to Pro. +// the rest move to their family's successor. A Basic ID never maps to Pro, +// so DeepSeek V4 Flash, which was Basic, moves to the Basic default. const RETIRED_MODEL_SUCCESSORS = new Map([ ["openai/gpt-5.6-luna", DEFAULT_MODEL], - ["deepseek/deepseek-v4-flash-0731", "deepseek/deepseek-v4.1-flash"], + ["deepseek/deepseek-v4-flash-0731", DEFAULT_MODEL], ["anthropic/claude-sonnet-5", DEFAULT_PRO_MODEL], ["anthropic/claude-opus-5", DEFAULT_PRO_MODEL], ["openai/gpt-5.6-sol", "openai/gpt-6.1-sol"], ["openai/gpt-5.6-terra", "openai/gpt-6.1-sol"], + ["meta/muse-spark-1.3", "meta/muse-spark-1.3-contributor"], ]); const MODEL_BY_ID = new Map(MODEL_CATALOG.map((entry) => [entry.id, entry])); diff --git a/supabase/functions/ai-stream/handler_test.ts b/supabase/functions/ai-stream/handler_test.ts index a6256c7d0..54e0c9ba5 100644 --- a/supabase/functions/ai-stream/handler_test.ts +++ b/supabase/functions/ai-stream/handler_test.ts @@ -107,10 +107,8 @@ Deno.test("streaming Ask forwards the shared prompt and returns provider SSE", a confidence: 0.96, probabilities: { "anthropic/claude-haiku-5.5": 0.96, - "z-ai/glm-5.3-flash": 0.01, "openai/gpt-6-luna": 0.02, - "google/gemini-3.8-flash": 0.01, - "deepseek/deepseek-v4.1-flash": 0.0, + "meta/muse-spark-1.3-contributor": 0.02, }, }, }, From 45c9cffca50c8e8cea39977e5117552f2892a0a9 Mon Sep 17 00:00:00 2001 From: Adam Date: Fri, 9 Oct 2026 17:25:56 +0000 Subject: [PATCH 6/7] Use current catalog models in the settings cleanup test GPT-5.6 Luna is now a retired ID that hydration rewrites to Claude Haiku 5.5, so the test's stand-in model has to be one that is still current. Co-Authored-By: Claude Opus 5.5 --- src/test/settings/settings-migration.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/test/settings/settings-migration.test.ts b/src/test/settings/settings-migration.test.ts index fb24a1033..e9bdc3bb8 100644 --- a/src/test/settings/settings-migration.test.ts +++ b/src/test/settings/settings-migration.test.ts @@ -51,7 +51,7 @@ import { useSettingsStore } from '@/lib/stores/settings-store'; const features = ['generate', 'attack', 'brainstorm', 'factoids', 'ask', 'sparring'] as const; const savedWithTemperature = Object.fromEntries(features.map((feature) => [ feature, - { provider: 'openrouter', model: 'openai/gpt-5.6-luna', temperature: 0.7, reasoningEffort: 'low' }, + { provider: 'openrouter', model: 'openai/gpt-6-luna', temperature: 0.7, reasoningEffort: 'low' }, ])); describe('settings hydration cleanup', () => { @@ -73,7 +73,7 @@ describe('settings hydration cleanup', () => { const saved = db.row.feature_configs as Record; expect(Object.keys(saved.ask)).not.toContain('temperature'); - expect(saved.ask.model).toBe('openai/gpt-5.6-luna'); + expect(saved.ask.model).toBe('openai/gpt-6-luna'); }); it('does not overwrite a settings save that lands before the cleanup', async () => { @@ -83,7 +83,7 @@ describe('settings hydration cleanup', () => { ...db.row, feature_configs: { ...savedWithTemperature, - ask: { provider: 'openrouter', model: 'google/gemini-3.8-flash', reasoningEffort: 'low' }, + ask: { provider: 'openrouter', model: 'meta/muse-spark-1.3-contributor', reasoningEffort: 'low' }, }, updated_at: '2026-10-09T17:00:00.000Z', }; @@ -92,6 +92,6 @@ describe('settings hydration cleanup', () => { await useSettingsStore.getState().hydrateFromServer(); const saved = db.row.feature_configs as Record; - expect(saved.ask.model).toBe('google/gemini-3.8-flash'); + expect(saved.ask.model).toBe('meta/muse-spark-1.3-contributor'); }); }); From 920154beaf54f262af280cf071455da77d51b6cf Mon Sep 17 00:00:00 2001 From: Adam Date: Fri, 9 Oct 2026 17:29:23 +0000 Subject: [PATCH 7/7] Correct the live-verify note now that Muse Spark contributor is a Basic model Co-Authored-By: Claude Opus 5.5 --- docs/testing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/testing.md b/docs/testing.md index 5d111b275..33dab4898 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -215,7 +215,7 @@ To check one feature by hand, or keep evidence for a change, use the same stack LIVE_PROVIDERS=1 LIVE_OPENROUTER_API_KEY=... LIVE_EXA_API_KEY=... ./.superset/db-gate.sh ``` -Live mode uses the same locked stack but skips pgTAP and the fake-provider specs. It runs `e2e/live/` on desktop Chromium against real OpenRouter, Jev, and Exa. Imported web pages are fetched live too. The fake provider becomes a recording proxy. It forwards every chat call to OpenRouter with the model pinned to `LIVE_MODEL` (default `meta/muse-spark-1.3-contributor`; CI reads the `LIVE_MODEL` repository variable, which is unset), passes Jev decisions through unchanged, and refuses calls past `MAX_UPSTREAM_CALLS` (200). The default is the cheapest model that supports every request parameter the app sends, including `response_format` and reasoning effort. It is test-only and not in the app catalog, so users cannot pick it. Meta is its only provider, so an outage there fails the gate instead of failing over. +Live mode uses the same locked stack but skips pgTAP and the fake-provider specs. It runs `e2e/live/` on desktop Chromium against real OpenRouter, Jev, and Exa. Imported web pages are fetched live too. The fake provider becomes a recording proxy. It forwards every chat call to OpenRouter with the model pinned to `LIVE_MODEL` (default `meta/muse-spark-1.3-contributor`; CI reads the `LIVE_MODEL` repository variable, which is unset), passes Jev decisions through unchanged, and refuses calls past `MAX_UPSTREAM_CALLS` (200). The default is the cheapest model that supports every request parameter the app sends, including `response_format` and reasoning effort. It is also the Basic catalog model "Muse Spark 1.3", so a live run that selects it cannot be told apart from the pin by the upstream model alone; recipes check the requested model instead. Meta is its only provider, so an outage there fails the gate instead of failing over. The specs cover web-researched case generation, verified Factoids citations, Ask with web search, URL import, streamed Ask, Attack, Brainstorm, a judged sparring round, and Crossfire with live Jev signals. Outputs vary, so tests assert structure rather than text: