diff --git a/e2e/animation-polish.spec.ts b/e2e/animation-polish.spec.ts index 41235dcad..63abc5f7d 100644 --- a/e2e/animation-polish.spec.ts +++ b/e2e/animation-polish.spec.ts @@ -2,7 +2,7 @@ import { expect, test, type Page } from '@playwright/test'; import { seedCurrentDebate, stubSupabaseSession } from './helpers/auth'; import { stubAIResponses } from './fixtures/ai-responses'; -const featureConfig = { provider: 'openrouter', model: 'z-ai/glm-5.3-flash', temperature: 0.7 }; +const featureConfig = { provider: 'openrouter', model: 'z-ai/glm-5.3-flash' }; const STATE_TIMEOUT_MS = 15_000; async function prepareMotionFixture(page: Page, reducedMotion: 'no-preference' | 'reduce') { diff --git a/e2e/authed-pages.spec.ts b/e2e/authed-pages.spec.ts index d4d7f3741..cd85d9540 100644 --- a/e2e/authed-pages.spec.ts +++ b/e2e/authed-pages.spec.ts @@ -100,7 +100,7 @@ test('visible Ask advanced controls use the non-streaming contract', async ({ pa outputLanguage: 'en', theme: 'light', featureConfigs: { - ask: { provider: 'openai', model: 'gpt-5.4-mini', temperature: 0.7 }, + ask: { provider: 'openai', model: 'gpt-5.4-mini' }, }, }, version: 0, diff --git a/packages/core/src/ai/providers/types.ts b/packages/core/src/ai/providers/types.ts index 8fcf04971..93ddcd638 100644 --- a/packages/core/src/ai/providers/types.ts +++ b/packages/core/src/ai/providers/types.ts @@ -26,6 +26,5 @@ export const AVAILABLE_MODELS = MODEL_CATALOG; export interface ProviderConfig { provider: string; model: string; - temperature: number; reasoningEffort?: ReasoningEffort; } diff --git a/src/lib/ai/client.ts b/src/lib/ai/client.ts index e75b969ad..d955fb405 100644 --- a/src/lib/ai/client.ts +++ b/src/lib/ai/client.ts @@ -541,7 +541,6 @@ async function callSupabaseAI( input, provider, model: config.model, - temperature: config.temperature, reasoningEffort: config.reasoningEffort ?? 'low', sourceIds, ...(debateContext ? { debateContext } : {}), @@ -1456,7 +1455,7 @@ function sparringRequestKey( voice = false, ): string { const authScope = getActiveUserId() ?? getActiveAccessToken() ?? 'signed-out'; - return JSON.stringify([authScope, workspaceContext?.workspaceId ?? null, debateId, input, config.provider, config.model, config.temperature, config.reasoningEffort, voice]); + return JSON.stringify([authScope, workspaceContext?.workspaceId ?? null, debateId, input, config.provider, config.model, config.reasoningEffort, voice]); } /** Generate the opponent's next speech for a validated sparring turn. */ diff --git a/src/lib/ai/streaming.ts b/src/lib/ai/streaming.ts index eaf144713..28cfb4c99 100644 --- a/src/lib/ai/streaming.ts +++ b/src/lib/ai/streaming.ts @@ -210,7 +210,6 @@ export async function streamAskChat( ...(debateContext ? { debateContext } : {}), provider, model: config.model, - temperature: config.temperature, reasoningEffort: config.reasoningEffort ?? 'low', }), signal: controller.signal, diff --git a/src/lib/stores/settings-store.ts b/src/lib/stores/settings-store.ts index 6c4c56610..9de5d6884 100644 --- a/src/lib/stores/settings-store.ts +++ b/src/lib/stores/settings-store.ts @@ -57,15 +57,14 @@ const LEGACY_MODEL_SUCCESSORS: Record = { const defaultConfig: ProviderConfig = { provider: DEFAULT_PROVIDER, model: DEFAULT_MODEL, - temperature: 0.7, reasoningEffort: 'low', }; const getDefaultFeatureConfigs = (model: string = DEFAULT_MODEL): Record => ({ generate: { ...defaultConfig, model }, attack: { ...defaultConfig, model }, - brainstorm: { ...defaultConfig, model, temperature: 0.9 }, - factoids: { ...defaultConfig, model, temperature: 0.3 }, + brainstorm: { ...defaultConfig, model }, + factoids: { ...defaultConfig, model }, ask: { ...defaultConfig, model }, sparring: { ...defaultConfig, model }, }); @@ -91,9 +90,6 @@ function normalizeFeatureConfig( ): ProviderConfig { const candidate = asRecord(value); const model = normalizeModelId(candidate?.model, fallback.model); - const temperature = typeof candidate?.temperature === 'number' && Number.isFinite(candidate.temperature) - ? candidate.temperature - : fallback.temperature; const reasoningEffort = isReasoningEffort(candidate?.reasoningEffort) ? candidate.reasoningEffort : fallback.reasoningEffort ?? 'low'; @@ -105,7 +101,6 @@ function normalizeFeatureConfig( : !allowProModels && getModelCatalogEntry(model)?.isProOnly ? DEFAULT_MODEL : model, - temperature, reasoningEffort, }; } @@ -129,7 +124,7 @@ function featureConfigsAreCanonical(value: unknown, expected: Record { const config = asRecord(saved[feature]); if (!config) return false; @@ -137,7 +132,6 @@ function featureConfigsAreCanonical(value: unknown, expected: Record allowedKeys.has(key)) && config.provider === target.provider && config.model === target.model - && config.temperature === target.temperature && config.reasoningEffort === target.reasoningEffort; }); } @@ -189,13 +183,22 @@ async function updateSettings(payload: { } } -async function persistMigratedFeatureConfigs(featureConfigs: Record) { +// The cleanup rewrites the whole feature_configs snapshot it read, so it only +// applies while the row is unchanged since that read. A settings save that +// lands first moves updated_at and wins; the next hydration retries. +async function persistMigratedFeatureConfigs( + featureConfigs: Record, + readUpdatedAt: string | null, +) { const userId = await getUserId(); if (!userId) return; - const { error } = await supabase + const update = supabase .from('user_settings') .update({ feature_configs: featureConfigs, updated_at: new Date().toISOString() }) .eq('user_id', userId); + const { error } = await (readUpdatedAt === null + ? update.is('updated_at', null) + : update.eq('updated_at', readUpdatedAt)); if (error) { console.warn('[SettingsStore] Failed to persist migrated settings:', error.message); } @@ -324,8 +327,11 @@ export const useSettingsStore = create()( feature_configs: mergedConfigs, custom_system_prompt: customSystemPrompt ?? null, }); - } else if (configsNeedMigration) { - await persistMigratedFeatureConfigs(mergedConfigs); + } else if (configsNeedMigration && get().featureConfigs === mergedConfigs) { + await persistMigratedFeatureConfigs( + mergedConfigs, + typeof data.updated_at === 'string' ? data.updated_at : null, + ); } }, resetLocalData: (preserveLanguage = false) => { diff --git a/src/test/ai/streaming.test.ts b/src/test/ai/streaming.test.ts index 4a50e3053..49b34ad04 100644 --- a/src/test/ai/streaming.test.ts +++ b/src/test/ai/streaming.test.ts @@ -41,7 +41,7 @@ function mockFetchOnce(body: BodyInit | null, init: ResponseInit = { status: 200 return fetchMock; } -const streamConfig = { provider: 'openrouter', model: 'gpt-5.4-mini', temperature: 0.7 } as const; +const streamConfig = { provider: 'openrouter', model: 'gpt-5.4-mini' } as const; const streamOptions = {}; beforeEach(() => { @@ -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', temperature: 0.7 })).toBe(true); - expect(isAskStreamingSupported({ provider: 'openai', model: 'gpt-5.4-mini', temperature: 0.7 })).toBe(false); - expect(isAskStreamingSupported({ provider: 'google', model: 'gemini-3-flash-preview', temperature: 0.7 })).toBe(false); - expect(isAskStreamingSupported({ provider: 'anthropic', model: 'claude-haiku-4-5-20251001', temperature: 0.7 })).toBe(false); - expect(isAskStreamingSupported({ provider: 'unknown' as string, model: 'x', temperature: 0.7 })).toBe(false); + expect(isAskStreamingSupported({ provider: 'openrouter', model: 'openai/gpt-5.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', temperature: 0.7 }, + { provider: 'openrouter', model: 'openai/gpt-5.6-luna' }, { citationMode: true } )).toBe(false); expect(isAskStreamingSupported( - { provider: 'openrouter', model: 'openai/gpt-5.6-luna', temperature: 0.7 }, + { provider: 'openrouter', model: 'openai/gpt-5.6-luna' }, { webSearch: true } )).toBe(false); expect(isAskStreamingSupported( - { provider: 'openrouter', model: 'openai/gpt-5.6-luna', temperature: 0.7 }, + { provider: 'openrouter', model: 'openai/gpt-5.6-luna' }, { prosConsMode: true } )).toBe(false); expect(isAskStreamingSupported( - { provider: 'openrouter', model: 'openai/gpt-5.6-luna', temperature: 0.7 }, + { provider: 'openrouter', model: 'openai/gpt-5.6-luna' }, { argumentMapMode: true } )).toBe(false); expect(isAskStreamingSupported( - { provider: 'openrouter', model: 'openai/gpt-5.6-luna', temperature: 0.7 }, + { provider: 'openrouter', model: 'openai/gpt-5.6-luna' }, { mode: 'clarify' } )).toBe(false); expect(isAskStreamingSupported( - { provider: 'openrouter', model: 'openai/gpt-5.6-luna', temperature: 0.7 }, + { provider: 'openrouter', model: 'openai/gpt-5.6-luna' }, { mode: 'general' } )).toBe(true); for (const mode of ['clarify', 'summarize', 'proscons', 'argumentmap']) { expect(isAskStreamingSupported( - { provider: 'openrouter', model: 'openai/gpt-5.6-luna', temperature: 0.7 }, + { provider: 'openrouter', model: 'openai/gpt-5.6-luna' }, { mode } )).toBe(false); } diff --git a/src/test/debate/debate-context.test.tsx b/src/test/debate/debate-context.test.tsx index 24a8669cf..64ac090eb 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', temperature: 0.7 }; + const config = { provider: 'openrouter' as const, model: 'openai/gpt-5.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/pages/CrossfireVoice.test.tsx b/src/test/pages/CrossfireVoice.test.tsx index 428d91615..75c62e1a6 100644 --- a/src/test/pages/CrossfireVoice.test.tsx +++ b/src/test/pages/CrossfireVoice.test.tsx @@ -24,7 +24,7 @@ vi.mock('@/lib/stores/session-store', () => ({ vi.mock('@/lib/stores/settings-store', () => ({ useSettingsStore: () => ({ - getFeatureConfig: () => ({ provider: 'google', model: 'gemini-3-flash-preview', temperature: 0.7 }), + getFeatureConfig: () => ({ provider: 'google', model: 'gemini-3-flash-preview' }), outputLanguage: 'es', }), })); diff --git a/src/test/pages/SparringVoice.test.tsx b/src/test/pages/SparringVoice.test.tsx index 713a099f7..8536100ee 100644 --- a/src/test/pages/SparringVoice.test.tsx +++ b/src/test/pages/SparringVoice.test.tsx @@ -37,7 +37,7 @@ vi.mock('@/lib/voice/opponent-voice', () => ({ }, })); -const CONFIG: ProviderConfig = { provider: 'mock', model: 'local', temperature: 0.4 }; +const CONFIG: ProviderConfig = { provider: 'mock', model: 'local' }; const SETTINGS = { outputLanguage: 'en' as const, getFeatureConfig: vi.fn((): ProviderConfig => CONFIG) }; vi.mock('@/lib/stores/settings-store', () => ({ diff --git a/src/test/settings/settings-migration.test.ts b/src/test/settings/settings-migration.test.ts new file mode 100644 index 000000000..fb24a1033 --- /dev/null +++ b/src/test/settings/settings-migration.test.ts @@ -0,0 +1,97 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +type Row = Record; + +const db = vi.hoisted(() => ({ + row: {} as Row, + onGetUser: undefined as (() => void) | undefined, +})); + +vi.mock('@/i18n', () => ({ applyAppLanguage: vi.fn(async () => undefined) })); + +vi.mock('@/lib/supabase/client', () => { + // One user_settings row. An update applies only when every filter matches, + // like a PostgREST UPDATE ... WHERE. + const update = (payload: Row) => { + const filters: Array<(row: Row) => boolean> = []; + const builder = { + eq(column: string, value: unknown) { + filters.push((row) => row[column] === value); + return builder; + }, + is(column: string, value: null) { + filters.push((row) => row[column] === value); + return builder; + }, + then(resolve: (result: { error: null }) => void) { + if (filters.every((matches) => matches(db.row))) db.row = { ...db.row, ...payload }; + resolve({ error: null }); + }, + }; + return builder; + }; + return { + supabase: { + auth: { + getUser: async () => { + db.onGetUser?.(); + return { data: { user: { id: 'user-1' } } }; + }, + }, + from: () => ({ + select: () => ({ single: async () => ({ data: { ...db.row }, error: null as null }) }), + update, + }), + }, + }; +}); + +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' }, +])); + +describe('settings hydration cleanup', () => { + beforeEach(() => { + db.onGetUser = undefined; + db.row = { + user_id: 'user-1', + subscription_tier: 'trial', + language: 'en', + output_language: 'en', + theme: 'system', + feature_configs: savedWithTemperature, + updated_at: '2026-10-01T00:00:00.000000+00:00', + }; + }); + + it('rewrites saved configs without temperature', async () => { + await useSettingsStore.getState().hydrateFromServer(); + + 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'); + }); + + it('does not overwrite a settings save that lands before the cleanup', async () => { + db.onGetUser = () => { + db.onGetUser = undefined; + db.row = { + ...db.row, + feature_configs: { + ...savedWithTemperature, + ask: { provider: 'openrouter', model: 'google/gemini-3.8-flash', reasoningEffort: 'low' }, + }, + updated_at: '2026-10-09T17:00:00.000Z', + }; + }; + + await useSettingsStore.getState().hydrateFromServer(); + + const saved = db.row.feature_configs as Record; + expect(saved.ask.model).toBe('google/gemini-3.8-flash'); + }); +}); diff --git a/supabase/functions/_shared/model-catalog.ts b/supabase/functions/_shared/model-catalog.ts index 6830c22b3..7cc8ae81e 100644 --- a/supabase/functions/_shared/model-catalog.ts +++ b/supabase/functions/_shared/model-catalog.ts @@ -11,21 +11,20 @@ export interface ModelCatalogEntry { readonly name: string; readonly maxTokens: number; readonly isProOnly: boolean; - readonly supportsTemperature: boolean; readonly supportedEfforts: readonly ReasoningEffort[]; } export const MODEL_CATALOG = [ - { id: "openai/gpt-5.6-luna", name: "GPT-5.6 Luna", maxTokens: 128_000, isProOnly: false, supportsTemperature: false, supportedEfforts: ["low", "medium", "high"] }, - { id: "google/gemini-3.8-flash", name: "Gemini 3.8 Flash", maxTokens: 65_536, isProOnly: false, supportsTemperature: true, supportedEfforts: ["low", "medium", "high"] }, - { id: "deepseek/deepseek-v4-flash-0731", name: "DeepSeek V4 Flash", maxTokens: 131_072, isProOnly: false, supportsTemperature: true, supportedEfforts: ["low", "high"] }, - { id: "anthropic/claude-sonnet-5", name: "Claude Sonnet 5", maxTokens: 128_000, isProOnly: true, supportsTemperature: false, supportedEfforts: ["low", "medium", "high"] }, - { id: "anthropic/claude-opus-5", name: "Claude Opus 5", maxTokens: 128_000, isProOnly: true, supportsTemperature: true, supportedEfforts: ["low", "medium", "high"] }, - { id: "openai/gpt-5.6-sol", name: "GPT-5.6 Sol", maxTokens: 128_000, isProOnly: true, supportsTemperature: false, supportedEfforts: ["low", "medium", "high"] }, - { id: "openai/gpt-5.6-terra", name: "GPT-5.6 Terra", maxTokens: 128_000, isProOnly: true, supportsTemperature: false, supportedEfforts: ["low", "medium", "high"] }, - { id: "deepseek/deepseek-v4-pro-0813", name: "DeepSeek V4 Pro", maxTokens: 384_000, isProOnly: true, supportsTemperature: true, supportedEfforts: ["low", "high"] }, - { id: "z-ai/glm-5.3-flash", name: "GLM-5.3 Flash", maxTokens: 131_072, isProOnly: true, supportsTemperature: true, supportedEfforts: ["low", "high"] }, - { id: "meta/muse-spark-1.3", name: "Muse Spark 1.3", maxTokens: 943_718, isProOnly: true, supportsTemperature: true, 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: "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"] }, + { 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"]; diff --git a/supabase/functions/_shared/research.ts b/supabase/functions/_shared/research.ts index 86a68b29d..9fd35b060 100644 --- a/supabase/functions/_shared/research.ts +++ b/supabase/functions/_shared/research.ts @@ -78,7 +78,6 @@ export interface ResearchDeps { model: string; systemPrompt: string; messages: Array<{ role: string; content: string }>; - temperature: number; reasoningEffort: ReasoningEffort; signal?: AbortSignal; }) => Promise<{ output: unknown }>; @@ -376,7 +375,6 @@ export async function decideAskSearch( model: deps.model, systemPrompt: ASK_SEARCH_DECISION_PROMPT, messages: [{ role: "user", content: prompt }], - temperature: 0, reasoningEffort: deps.reasoningEffort, signal: deps.signal, }); @@ -415,7 +413,6 @@ async function generateFollowUpQueries( model: deps.model, systemPrompt: QUERY_GENERATION_PROMPT, messages: [{ role: "user", content: prompt }], - temperature: 0.3, reasoningEffort: deps.reasoningEffort, signal: deps.signal, }); diff --git a/supabase/functions/ai-stream/index.ts b/supabase/functions/ai-stream/index.ts index 195baebfb..34d940e7c 100644 --- a/supabase/functions/ai-stream/index.ts +++ b/supabase/functions/ai-stream/index.ts @@ -228,7 +228,6 @@ export async function handleRequest(req: Request): Promise { if (!providerKey) return jsonResponse({ error: "OpenRouter is not configured" }, 500, corsHeaders); const history = normalizeHistory(input.history); - const temperature = typeof body.temperature === "number" ? body.temperature : 0.7; const startedAt = Date.now(); const stream = new ReadableStream({ @@ -271,7 +270,6 @@ export async function handleRequest(req: Request): Promise { stream: true, stream_options: { include_usage: true }, }; - if (modelEntry.supportsTemperature) payload.temperature = temperature; const response = await fetch(`${OPENROUTER_API_URL}/v1/chat/completions`, { method: "POST", diff --git a/supabase/functions/ai/features/ask.ts b/supabase/functions/ai/features/ask.ts index 30e7ebc7d..c6f81bffd 100644 --- a/supabase/functions/ai/features/ask.ts +++ b/supabase/functions/ai/features/ask.ts @@ -22,7 +22,7 @@ const MAX_ASK_CONTEXT_LENGTH = 32000; // ASK: Optionally run web search and include URL context from chat export async function handleAsk(ctx: FeatureContext): Promise { const feature = "ask" as const; - const { userId, provider, model, temperature, reasoningEffort, workSignal, researchDeps, startTime, researchUsageForLog, recordMainResult, buildSystemPromptFor, input: sanitizedInput } = ctx; + const { userId, provider, model, reasoningEffort, workSignal, researchDeps, startTime, researchUsageForLog, recordMainResult, buildSystemPromptFor, input: sanitizedInput } = ctx; const history = (sanitizedInput.history as Array<{ role: string; content: string }>) || []; const message = String(sanitizedInput.message || ""); const mode = String(sanitizedInput.mode || "general"); @@ -97,7 +97,7 @@ export async function handleAsk(ctx: FeatureContext): Promise { const systemPrompt = await buildSystemPromptFor(feature, effectiveResearch); const prompt = `${basePrompt}${urlContext}${webSearchContext}`; const messages = [...history, { role: "user", content: prompt }]; - const aiResult = recordMainResult(await callAI(provider, model, systemPrompt, messages, temperature, reasoningEffort, workSignal)); + const aiResult = recordMainResult(await callAI(provider, model, systemPrompt, messages, reasoningEffort, workSignal)); const output = cleanGeneratedText(aiResult.output); if (output && typeof output === "object") { diff --git a/supabase/functions/ai/features/attack.ts b/supabase/functions/ai/features/attack.ts index 09accb227..bce235cfe 100644 --- a/supabase/functions/ai/features/attack.ts +++ b/supabase/functions/ai/features/attack.ts @@ -15,7 +15,7 @@ import type { FeatureContext } from "./types.ts"; // ATTACK: Use deep research for evidence-based critique export async function handleAttack(ctx: FeatureContext): Promise { const feature = "attack" as const; - const { userId, provider, model, temperature, reasoningEffort, workSignal, researchDeps, startTime, researchUsageForLog, recordMainResult, buildSystemPromptFor, input: sanitizedInput } = ctx; + const { userId, provider, model, reasoningEffort, workSignal, researchDeps, startTime, researchUsageForLog, recordMainResult, buildSystemPromptFor, input: sanitizedInput } = ctx; const caseText = String(sanitizedInput.caseText || ""); const detectFallacies = typeof sanitizedInput.detectFallacies === "boolean" ? sanitizedInput.detectFallacies : true; const analyzeWeighing = typeof sanitizedInput.analyzeWeighing === "boolean" ? sanitizedInput.analyzeWeighing : true; @@ -28,7 +28,7 @@ export async function handleAttack(ctx: FeatureContext): Promise { const systemPrompt = await buildSystemPromptFor(feature, research); const userPrompt = buildUserPrompt(feature, sanitizedInput, searchContext); const messages = [{ role: "user", content: userPrompt }]; - const aiResult = recordMainResult(await callAI(provider, model, systemPrompt, messages, temperature, reasoningEffort, workSignal)); + const aiResult = recordMainResult(await callAI(provider, model, systemPrompt, messages, reasoningEffort, workSignal)); const output = cleanGeneratedText(aiResult.output); enforceAttackControls(output, detectFallacies, analyzeWeighing); diff --git a/supabase/functions/ai/features/brainstorm.ts b/supabase/functions/ai/features/brainstorm.ts index ddfb0652b..c889d93af 100644 --- a/supabase/functions/ai/features/brainstorm.ts +++ b/supabase/functions/ai/features/brainstorm.ts @@ -15,7 +15,7 @@ import type { FeatureContext } from "./types.ts"; // BRAINSTORM: Use research to inform argument angles export async function handleBrainstorm(ctx: FeatureContext): Promise { const feature = "brainstorm" as const; - const { userId, provider, model, temperature, reasoningEffort, workSignal, researchDeps, startTime, researchUsageForLog, recordMainResult, buildSystemPromptFor, input: sanitizedInput } = ctx; + const { userId, provider, model, reasoningEffort, workSignal, researchDeps, startTime, researchUsageForLog, recordMainResult, buildSystemPromptFor, input: sanitizedInput } = ctx; const motion = String(sanitizedInput.motion || sanitizedInput.topic || ""); const side = String(sanitizedInput.side || "both"); const outputMode = String(sanitizedInput.outputMode || "topfive"); @@ -29,7 +29,7 @@ export async function handleBrainstorm(ctx: FeatureContext): Promise { const systemPrompt = await buildSystemPromptFor(feature, research); const userPrompt = buildUserPrompt(feature, sanitizedInput, searchContext); const messages = [{ role: "user", content: userPrompt }]; - const aiResult = recordMainResult(await callAI(provider, model, systemPrompt, messages, temperature, reasoningEffort, workSignal)); + const aiResult = recordMainResult(await callAI(provider, model, systemPrompt, messages, reasoningEffort, workSignal)); const output = cleanGeneratedText(aiResult.output); enforceBrainstormControls(output, side, outputMode); diff --git a/supabase/functions/ai/features/crossfire.ts b/supabase/functions/ai/features/crossfire.ts index 52a344182..31ee7286f 100644 --- a/supabase/functions/ai/features/crossfire.ts +++ b/supabase/functions/ai/features/crossfire.ts @@ -29,7 +29,7 @@ export interface CrossfireHandlerOptions { // apply, and the combined prompt stays inside a fixed character budget. export async function handleCrossfire(ctx: FeatureContext, options: CrossfireHandlerOptions): Promise { const feature = "crossfire" as const; - const { userId, provider, model, temperature, reasoningEffort, workSignal, startTime, recordMainResult } = ctx; + const { userId, provider, model, reasoningEffort, workSignal, startTime, recordMainResult } = ctx; const { input: crossfireInput, sources: crossfireSources, workspaceSources } = options; if (!crossfireInput) throw new Error("Crossfire input was not validated"); @@ -62,7 +62,6 @@ export async function handleCrossfire(ctx: FeatureContext, options: CrossfireHan model, systemPrompt, [{ role: "user", content: userPrompt }], - temperature, reasoningEffort, workSignal, )); diff --git a/supabase/functions/ai/features/factoids.ts b/supabase/functions/ai/features/factoids.ts index a8f224b76..13f14f690 100644 --- a/supabase/functions/ai/features/factoids.ts +++ b/supabase/functions/ai/features/factoids.ts @@ -16,7 +16,7 @@ import type { FeatureContext } from "./types.ts"; // FACTOIDS: Use deep research with multiple searches export async function handleFactoids(ctx: FeatureContext): Promise { const feature = "factoids" as const; - const { userId, provider, model, temperature, reasoningEffort, workSignal, researchDeps, startTime, researchUsageForLog, recordMainResult, buildSystemPromptFor, input: sanitizedInput } = ctx; + const { userId, provider, model, reasoningEffort, workSignal, researchDeps, startTime, researchUsageForLog, recordMainResult, buildSystemPromptFor, input: sanitizedInput } = ctx; const claim = String(sanitizedInput.claim || ""); const region = String(sanitizedInput.region || "Global"); const preferredSourceType = String(sanitizedInput.sourceType || "any"); @@ -34,7 +34,7 @@ export async function handleFactoids(ctx: FeatureContext): Promise { const userPrompt = buildUserPrompt(feature, sanitizedInput, searchContext); const messages = [{ role: "user", content: userPrompt }]; - const aiResult = recordMainResult(await callAI(provider, model, systemPrompt, messages, temperature, reasoningEffort, workSignal)); + const aiResult = recordMainResult(await callAI(provider, model, systemPrompt, messages, reasoningEffort, workSignal)); const output = cleanGeneratedText(aiResult.output); // Post-process: quality floor. Only citations whose URL matches fetched, diff --git a/supabase/functions/ai/features/generate.ts b/supabase/functions/ai/features/generate.ts index 76accb016..abd97ea6f 100644 --- a/supabase/functions/ai/features/generate.ts +++ b/supabase/functions/ai/features/generate.ts @@ -26,7 +26,7 @@ import type { FeatureContext } from "./types.ts"; // GENERATE: Use deep research for evidence-backed arguments export async function handleGenerate(ctx: FeatureContext): Promise { const feature = "generate" as const; - const { req, userId, provider, model, temperature, reasoningEffort, workSignal, sources, sourceAssessments, sourceDecisionCandidates, researchDeps, startTime, researchUsageForLog, recordMainResult, buildSystemPromptFor, input: sanitizedInput } = ctx; + const { req, userId, provider, model, reasoningEffort, workSignal, sources, sourceAssessments, sourceDecisionCandidates, researchDeps, startTime, researchUsageForLog, recordMainResult, buildSystemPromptFor, input: sanitizedInput } = ctx; const motion = String(sanitizedInput.motion || sanitizedInput.topic || ""); const side = String(sanitizedInput.side || "pro"); const format = String(sanitizedInput.format || "custom"); @@ -81,7 +81,7 @@ export async function handleGenerate(ctx: FeatureContext): Promise { const systemPrompt = await buildSystemPromptFor(feature, research); const userPrompt = buildUserPrompt(feature, sanitizedInput, searchContext); const messages = [{ role: "user", content: userPrompt }]; - const aiResult = recordMainResult(await callAI(provider, model, systemPrompt, messages, temperature, reasoningEffort, workSignal)); + const aiResult = recordMainResult(await callAI(provider, model, systemPrompt, messages, reasoningEffort, workSignal)); const output = cleanGeneratedText(aiResult.output); // Preserve model evidence warnings when research exists. If research diff --git a/supabase/functions/ai/features/sparring.ts b/supabase/functions/ai/features/sparring.ts index 39d8e5472..36ccaffaf 100644 --- a/supabase/functions/ai/features/sparring.ts +++ b/supabase/functions/ai/features/sparring.ts @@ -28,14 +28,14 @@ export interface SparringHandlerOptions { // branch runs here, and score persistence follows output validation. export async function handleSparring(ctx: FeatureContext, options: SparringHandlerOptions): Promise { const feature = "sparring" as const; - const { userId, provider, model, temperature, reasoningEffort, workSignal, startTime, researchUsageForLog, recordMainResult, buildSystemPromptFor } = ctx; + const { userId, provider, model, reasoningEffort, workSignal, startTime, researchUsageForLog, recordMainResult, buildSystemPromptFor } = ctx; const { request: sparringRequest, database: sparringDatabase, claimToken: sparringScoreClaimToken } = options; if (!sparringRequest) throw new Error("Invalid sparring request"); const systemPrompt = await buildSystemPromptFor(feature); const userPrompt = buildUserPrompt(feature, sparringRequest as unknown as Record, ""); const messages = [{ role: "user", content: userPrompt }]; - const aiResult = recordMainResult(await callAI(provider, model, systemPrompt, messages, temperature, reasoningEffort, workSignal)); + const aiResult = recordMainResult(await callAI(provider, model, systemPrompt, messages, reasoningEffort, workSignal)); const output = cleanGeneratedText(aiResult.output); const normalizedOutput = normalizeSparringOutput(output, sparringRequest.action); const parsedOutput = parseSparringProviderOutput(normalizedOutput, sparringRequest.action); diff --git a/supabase/functions/ai/features/types.ts b/supabase/functions/ai/features/types.ts index 14d63b729..c5c9c6d7f 100644 --- a/supabase/functions/ai/features/types.ts +++ b/supabase/functions/ai/features/types.ts @@ -14,7 +14,6 @@ export interface FeatureContext { userId: string; provider: Provider; model: string; - temperature: number; reasoningEffort: ReasoningEffort; /** Caller cancellation combined with the shared request budget. */ workSignal: AbortSignal; diff --git a/supabase/functions/ai/handler-research.test.ts b/supabase/functions/ai/handler-research.test.ts index 7732ba838..9e4d03478 100644 --- a/supabase/functions/ai/handler-research.test.ts +++ b/supabase/functions/ai/handler-research.test.ts @@ -727,7 +727,9 @@ Deno.test("AI handlers use strict OpenRouter routing and server-side reasoning t if (reasoning.exclude !== true || provider.require_parameters !== true) throw new Error(`strict reasoning route missing for ${entry.id}`); if ("sort" in provider || "order" in provider) throw new Error(`sort/order disabled balanced routing for ${entry.id}`); if ((body.response_format as Record)?.type !== "json_object") throw new Error(`JSON response format missing for ${entry.id}`); - if (entry.supportsTemperature !== ("temperature" in body)) throw new Error(`temperature support mismatch for ${entry.id}`); + // With require_parameters, one endpoint without temperature support + // turns the whole request into a routing 404 (Claude Opus 5 did). + if ("temperature" in body) throw new Error(`temperature was sent for ${entry.id}`); } for (const [offset, effort] of ["low", "medium", "high"].entries()) { const body = openRouterBodies[MODEL_CATALOG.length + offset]; diff --git a/supabase/functions/ai/index.ts b/supabase/functions/ai/index.ts index af3ec5264..34281ee4a 100644 --- a/supabase/functions/ai/index.ts +++ b/supabase/functions/ai/index.ts @@ -159,7 +159,6 @@ export async function handleRequest(req: Request): Promise { ? undefined : isReasoningEffort(body.reasoningEffort) ? body.reasoningEffort : null; if (requestedReasoningEffort === null) return badRequest("Invalid reasoning effort", corsHeaders); - const temperature = typeof body.temperature === "number" ? body.temperature : 0.7; // A voice round. It sits beside `input` so servers from before voice ignore it. const voiceRequested = body.voice === true; // The per-user custom system prompt was replaced by each Current Debate's @@ -610,7 +609,6 @@ export async function handleRequest(req: Request): Promise { model: modelEntry.id, requestedReasoningEffort: requestedReasoningEffort ?? "medium", reasoningEffort, - supportsTemperature: modelEntry.supportsTemperature, }); const startTime = Date.now(); @@ -628,7 +626,7 @@ export async function handleRequest(req: Request): Promise { }, callModel: async (args) => { try { - const result = await callAI(args.provider, args.model, args.systemPrompt, args.messages, args.temperature, args.reasoningEffort, args.signal); + const result = await callAI(args.provider, args.model, args.systemPrompt, args.messages, args.reasoningEffort, args.signal); // A provider that ignores cancellation must not append usage after the // research timeout has already been finalized by the parent request. if (result.usage && !args.signal?.aborted) researchUsage.push(result.usage); @@ -683,7 +681,6 @@ export async function handleRequest(req: Request): Promise { userId, provider, model, - temperature, reasoningEffort, workSignal, voice: voiceRequested, @@ -748,7 +745,7 @@ export async function handleRequest(req: Request): Promise { const systemPrompt = await buildSystemPromptFor(feature); const userPrompt = buildUserPrompt(feature, sanitizedInput, ""); const messages = [{ role: "user", content: userPrompt }]; - const fallbackResult = recordMainResult(await callAI(provider, model, systemPrompt, messages, temperature, reasoningEffort, workSignal)); + const fallbackResult = recordMainResult(await callAI(provider, model, systemPrompt, messages, reasoningEffort, workSignal)); const output = cleanGeneratedText(fallbackResult.output); const latencyMs = Date.now() - startTime; diff --git a/supabase/functions/ai/provider.ts b/supabase/functions/ai/provider.ts index cada9d4ee..024e35dcd 100644 --- a/supabase/functions/ai/provider.ts +++ b/supabase/functions/ai/provider.ts @@ -101,12 +101,11 @@ export async function callAI( model: string, systemPrompt: string, messages: Array<{ role: string; content: string }>, - temperature: number, reasoningEffort: ReasoningEffort, signal?: AbortSignal ): Promise { try { - return await callAIOnce(provider, model, systemPrompt, messages, temperature, reasoningEffort, signal); + return await callAIOnce(provider, model, systemPrompt, messages, reasoningEffort, signal); } catch (err) { if (!(err instanceof InvalidJsonResponseError)) throw attachMissingUsage(err, "call"); @@ -118,7 +117,6 @@ export async function callAI( model, `${systemPrompt}\n\nJSON REPAIR INSTRUCTION: Return only valid JSON matching this schema, no prose.`, withJsonRepairPrompt(messages), - 0, reasoningEffort, signal, ); @@ -169,7 +167,6 @@ export async function callAIOnce( model: string, systemPrompt: string, messages: Array<{ role: string; content: string }>, - temperature: number, reasoningEffort: ReasoningEffort, signal?: AbortSignal ): Promise { @@ -177,7 +174,6 @@ export async function callAIOnce( return await callOpenRouter( [{ role: "system", content: systemPrompt }, ...messages], model, - temperature, reasoningEffort, signal, ); @@ -199,7 +195,6 @@ export function createRequestBudgetSignal( export async function callOpenRouter( messages: Array<{ role: string; content: string }>, model: string, - temperature: number, reasoningEffort: ReasoningEffort, signal?: AbortSignal, ): Promise { @@ -216,7 +211,6 @@ export async function callOpenRouter( // Omitting sort and order keeps OpenRouter's balanced provider load balancing. provider: { require_parameters: true }, }; - if (modelEntry.supportsTemperature) payload.temperature = temperature; const { response, data, errorBody } = await fetchJsonWithTimeout(`${OPENROUTER_API_URL}/v1/chat/completions`, { method: "POST",