Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion e2e/animation-polish.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down
2 changes: 1 addition & 1 deletion e2e/authed-pages.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 0 additions & 1 deletion packages/core/src/ai/providers/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,5 @@ export const AVAILABLE_MODELS = MODEL_CATALOG;
export interface ProviderConfig {
provider: string;
model: string;
temperature: number;
reasoningEffort?: ReasoningEffort;
}
3 changes: 1 addition & 2 deletions src/lib/ai/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -541,7 +541,6 @@ async function callSupabaseAI<T = unknown>(
input,
provider,
model: config.model,
temperature: config.temperature,
reasoningEffort: config.reasoningEffort ?? 'low',
sourceIds,
...(debateContext ? { debateContext } : {}),
Expand Down Expand Up @@ -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. */
Expand Down
1 change: 0 additions & 1 deletion src/lib/ai/streaming.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,6 @@ export async function streamAskChat(
...(debateContext ? { debateContext } : {}),
provider,
model: config.model,
temperature: config.temperature,
reasoningEffort: config.reasoningEffort ?? 'low',
}),
signal: controller.signal,
Expand Down
32 changes: 19 additions & 13 deletions src/lib/stores/settings-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,14 @@ const LEGACY_MODEL_SUCCESSORS: Record<string, string> = {
const defaultConfig: ProviderConfig = {
provider: DEFAULT_PROVIDER,
model: DEFAULT_MODEL,
temperature: 0.7,
reasoningEffort: 'low',
};

const getDefaultFeatureConfigs = (model: string = DEFAULT_MODEL): Record<FeatureTab, ProviderConfig> => ({
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 },
});
Expand All @@ -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';
Expand All @@ -105,7 +101,6 @@ function normalizeFeatureConfig(
: !allowProModels && getModelCatalogEntry(model)?.isProOnly
? DEFAULT_MODEL
: model,
temperature,
reasoningEffort,
};
}
Expand All @@ -129,15 +124,14 @@ function featureConfigsAreCanonical(value: unknown, expected: Record<FeatureTab,
const features = Object.keys(expected) as FeatureTab[];
if (!saved || Object.keys(saved).length !== features.length) return false;

const allowedKeys = new Set(['provider', 'model', 'temperature', 'reasoningEffort']);
const allowedKeys = new Set(['provider', 'model', 'reasoningEffort']);
return features.every((feature) => {
const config = asRecord(saved[feature]);
if (!config) return false;
const target = expected[feature];
return Object.keys(config).every((key) => allowedKeys.has(key))
&& config.provider === target.provider
&& config.model === target.model
&& config.temperature === target.temperature
&& config.reasoningEffort === target.reasoningEffort;
});
}
Expand Down Expand Up @@ -189,13 +183,22 @@ async function updateSettings(payload: {
}
}

async function persistMigratedFeatureConfigs(featureConfigs: Record<FeatureTab, ProviderConfig>) {
// 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<FeatureTab, ProviderConfig>,
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);
}
Expand Down Expand Up @@ -324,8 +327,11 @@ export const useSettingsStore = create<SettingsState>()(
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) => {
Expand Down
26 changes: 13 additions & 13 deletions src/test/ai/streaming.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down Expand Up @@ -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);
}
Expand Down
2 changes: 1 addition & 1 deletion src/test/debate/debate-context.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown> }])[1].body;

it('sends IDs only, and a Review choice lasts for exactly one successful request', async () => {
Expand Down
2 changes: 1 addition & 1 deletion src/test/pages/CrossfireVoice.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
}),
}));
Expand Down
2 changes: 1 addition & 1 deletion src/test/pages/SparringVoice.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => ({
Expand Down
97 changes: 97 additions & 0 deletions src/test/settings/settings-migration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';

type Row = Record<string, unknown>;

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<string, Row>;
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<string, Row>;
expect(saved.ask.model).toBe('google/gemini-3.8-flash');
});
});
21 changes: 10 additions & 11 deletions supabase/functions/_shared/model-catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"];
Expand Down
3 changes: 0 additions & 3 deletions supabase/functions/_shared/research.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }>;
Expand Down Expand Up @@ -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,
});
Expand Down Expand Up @@ -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,
});
Expand Down
2 changes: 0 additions & 2 deletions supabase/functions/ai-stream/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,6 @@ export async function handleRequest(req: Request): Promise<Response> {
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({
Expand Down Expand Up @@ -271,7 +270,6 @@ export async function handleRequest(req: Request): Promise<Response> {
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",
Expand Down
Loading
Loading