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:
+10
-25
@@ -86,11 +86,10 @@ describe("the model catalog projection", () => {
|
||||
// the record is empty (defaults/slots NULL), on the owner's machine
|
||||
// configured slots must surface exactly as configured.
|
||||
const record = readSettings();
|
||||
const chat = record?.chat;
|
||||
const strong = effectiveSettings().models.strong;
|
||||
if (chat?.model || strong) {
|
||||
assert.ok(defaults, "a configured chain yields a default");
|
||||
assert.deepEqual(defaults!.model, chat?.model ?? { provider: strong!.provider, id: strong!.id });
|
||||
if (strong) {
|
||||
assert.ok(defaults, "a configured strong slot yields a default");
|
||||
assert.deepEqual(defaults!.model, { provider: strong.provider, id: strong.id });
|
||||
} else {
|
||||
assert.equal(defaults, null);
|
||||
}
|
||||
@@ -105,29 +104,15 @@ describe("the model catalog projection", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("defaults: the chat section over the strong slot; null when both empty (BYOK)", () => {
|
||||
assert.equal(catalogDefaultsFor(undefined, null), null);
|
||||
it("defaults: the strong slot verbatim; null when unset (BYOK, per-session models 2026-09-28)", () => {
|
||||
assert.equal(catalogDefaultsFor(null), null);
|
||||
const strong = { provider: "openrouter", id: "z-ai/glm-5.3-flash", thinkingLevel: "low" as const };
|
||||
const d = catalogDefaultsFor(undefined, strong);
|
||||
const d = catalogDefaultsFor(strong);
|
||||
assert.deepEqual(d, { model: { provider: "openrouter", id: "z-ai/glm-5.3-flash" }, thinkingLevel: "low" });
|
||||
assert.deepEqual(catalogDefaultsFor({ model: { provider: "openrouter", id: "qwen/qwen3.8-flash" }, thinkingLevel: "high" }, strong), {
|
||||
model: { provider: "openrouter", id: "qwen/qwen3.8-flash" },
|
||||
thinkingLevel: "high",
|
||||
});
|
||||
// partial: only the model chosen → level falls back to the slot's
|
||||
const partial = catalogDefaultsFor({ model: { provider: "openrouter", id: "qwen/qwen3.8-flash" } }, strong);
|
||||
assert.ok(partial);
|
||||
assert.equal(partial!.thinkingLevel, "low");
|
||||
// thinkingLevel-only record: model untouched
|
||||
assert.deepEqual(catalogDefaultsFor({ thinkingLevel: "minimal" }, strong), {
|
||||
model: { provider: "openrouter", id: "z-ai/glm-5.3-flash" },
|
||||
thinkingLevel: "minimal",
|
||||
});
|
||||
// chat.model with NO strong slot: the conversation runs on it alone
|
||||
assert.deepEqual(
|
||||
catalogDefaultsFor({ model: { provider: "openrouter", id: "qwen/qwen3.8-flash" } }, null),
|
||||
{ model: { provider: "openrouter", id: "qwen/qwen3.8-flash" } },
|
||||
);
|
||||
// No level stored → the model alone reports (the level resolves at
|
||||
// engine build from DEFAULT_THINKING_ORDER).
|
||||
const bare = catalogDefaultsFor({ provider: "openrouter", id: "qwen/qwen3.8-flash" });
|
||||
assert.deepEqual(bare, { model: { provider: "openrouter", id: "qwen/qwen3.8-flash" } });
|
||||
});
|
||||
|
||||
it("toModelDto projects the raw catalog model verbatim", () => {
|
||||
|
||||
Reference in New Issue
Block a user