per-session models: every session owns its model + thinking level from birth
The reported bug (2026-09-28): every chat ran glm-5.3-flash regardless of any pick, while composer and settings UIs agreed with each other and lied about the run. Root cause: a fossil default-settings record (settings.json chat section, frozen pre-slots-wave) was stamped verbatim into every new chat at birth by the new-session flow, and resolveConversationSpec prefers the per-chat record over the strong slot forever. Owner redesign — models are per session, always; the strong slot is the new-session default: - POST /api/chats stamps the newborn verbatim from the strong slot (model + thinking level); no client settings PUT on create anymore - the composer control is dual-bound: in-session it reads/writes the session's own record (GET /api/chats/:id, PUT .../settings); on the draft surfaces it binds the strong slot (picking there = changing the default) - thinking levels follow the identical mechanics (confirmed by owner) - the zombie default-settings system dies whole: useDefaultSettings, /api/settings/defaults, src/store/default-settings.ts, the chat section of UserSettings (sanitizer strips fossil copies), the legacy default-settings.json fold, and DEFAULT_SETTINGS_FILE - catalogDefaultsFor collapses to the strong slot alone - thinking display for a legacy unstamped session resolves exactly like the engine (session ?? strong ?? model ladder) Tests: new chat-birth-stamp suite pins the stamp, the unconfigured boot, the unset level, and the copy-not-live-view regression; defaults pins rewritten strong-only; legacy-migration suite deleted.
This commit is contained in:
@@ -11,6 +11,10 @@ import type { ChatSettings } from "@/src/core";
|
||||
/**
|
||||
* The per-chat model control (model-control wave): replace-whole settings —
|
||||
* the picker sends the complete desired object ({} clears all overrides).
|
||||
* PER-SESSION MODELS (owner, 2026-09-28): this route is THE in-session
|
||||
* writer — the composer's control binds the session's own record; the
|
||||
* strong slot is only the birth default and never reaches an existing
|
||||
* session again.
|
||||
* Validation is the catalog gate (src/server/model-catalog.ts): unknown
|
||||
* provider/model is a 400. Thinking levels never reject (owner,
|
||||
* interlude 2026-09-22): a pinned level clamps to the named model's
|
||||
|
||||
+15
-1
@@ -1,4 +1,4 @@
|
||||
import { createChat, isTurnRunning, listChats, titleFromPrompt } from "@/src/server";
|
||||
import { createChat, isTurnRunning, listChats, readSettingsRecord, stores, titleFromPrompt } from "@/src/server";
|
||||
import type { ChatListItem } from "@/src/core";
|
||||
|
||||
/** The session picker's list: record-derived summaries (phase, pending
|
||||
@@ -20,5 +20,19 @@ export async function POST(req: Request) {
|
||||
const body = (await req.json().catch(() => ({}))) as { title?: unknown };
|
||||
const title = typeof body.title === "string" ? body.title : "";
|
||||
const chat = createChat(titleFromPrompt(title || "Untitled session"));
|
||||
// PER-SESSION MODELS (owner, 2026-09-28): a session owns its model +
|
||||
// thinking level from birth — stamped verbatim from the STRONG slot,
|
||||
// the new-session default. From here the strong slot never reaches
|
||||
// this session again: in-session picks write the session's own record
|
||||
// (PUT /api/chats/:id/settings). No strong slot configured (fresh BYOK
|
||||
// boot) → no stamp; the turn gate's 503 models_not_configured envelope
|
||||
// refuses honestly until the setup flow fills the slot.
|
||||
const strong = readSettingsRecord()?.models?.strong;
|
||||
if (strong?.model) {
|
||||
stores.chatStore.saveSettings(chat.id, {
|
||||
model: { provider: strong.model.provider, id: strong.model.id },
|
||||
thinkingLevel: strong.thinkingLevel,
|
||||
});
|
||||
}
|
||||
return Response.json({ id: chat.id, title: chat.title });
|
||||
}
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
/**
|
||||
* GET/PUT /api/settings/defaults — server persistence for the DEFAULT
|
||||
* settings: THE default model + thinking level + knowledge-context
|
||||
* budget. The new-session surfaces' controls (the home graph view's
|
||||
* topic dock and /chat/new) ARE the settings page for the default model
|
||||
* (owner, 2026-09-18): there is no baked-in default — BYOK first boot
|
||||
* renders "no model" until the user picks one, and the last choice IS
|
||||
* the default from then on. Both surfaces share ONE record; the file
|
||||
* lives at data/default-settings.json (config.DEFAULT_SETTINGS_FILE).
|
||||
*
|
||||
* GET → 200 {settings} once saved, 200 null on first boot ("no model").
|
||||
* PUT → 200 {settings} — the VALIDATED copy, level-clamped, persisted.
|
||||
* The exact per-chat contract (parseSettingsBody + the catalog
|
||||
* gate): an unknown provider/model is a 400. Thinking levels
|
||||
* never reject (owner, interlude 2026-09-22): a pinned level
|
||||
* clamps to the named model's nearest supported and the clamped
|
||||
* value is stored — model swaps just work.
|
||||
*
|
||||
* Per-session settings (PUT /api/chats/:id/settings) are a separate record
|
||||
* untouched by this — the default still rides along on session creation
|
||||
* exactly as before.
|
||||
*/
|
||||
import {
|
||||
clampChatLevel,
|
||||
effectiveSettings,
|
||||
parseSettingsBody,
|
||||
readDefaultSettings,
|
||||
validateChatSettings,
|
||||
writeDefaultSettings,
|
||||
} from "@/src/server";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET() {
|
||||
return Response.json(readDefaultSettings());
|
||||
}
|
||||
|
||||
export async function PUT(request: Request) {
|
||||
const body = await request.json().catch(() => null);
|
||||
const parsed = parseSettingsBody(body);
|
||||
if (!parsed.ok) return Response.json({ error: parsed.error }, { status: 400 });
|
||||
const verdict = validateChatSettings(parsed.settings);
|
||||
if (!verdict.ok) {
|
||||
return Response.json({ error: verdict.error, supported: verdict.supported }, { status: 400 });
|
||||
}
|
||||
// Clamp-on-write (owner, interlude 2026-09-22): a pinned thinking
|
||||
// level never rejects the write — it clamps to the named model's
|
||||
// nearest supported level (body model, else the record's strong
|
||||
// slot) and the clamped value is stored.
|
||||
const settings = clampChatLevel(parsed.settings, effectiveSettings().models.strong);
|
||||
writeDefaultSettings(settings);
|
||||
return Response.json({ settings });
|
||||
}
|
||||
Reference in New Issue
Block a user