Skip to content

Commit b33deb4

Browse files
clawsweeper[bot]TurboTheTurtlevincentkocTakhoffman
authored
fix(sessions): preserve compatible auth overrides (#85014)
Summary: - This replacement branch preserves compatible session auth profile overrides during `sessions.patch` model ch ... d/cross-provider regression coverage, and updates related doctor/Mantis test assertions plus the changelog. - Reproducibility: yes. by source inspection: current main’s `sessions.patch` model branch calls `applyModelOv ... d helper clears auth fields unless preservation is requested. I did not run tests in this read-only review. Automerge notes: - PR branch already contained follow-up commit before automerge: test(mantis): align telegram proof evidence comment - PR branch already contained follow-up commit before automerge: fix(sessions): preserve provider auth aliases - PR branch already contained follow-up commit before automerge: fix(sessions): guard unprefixed auth overrides - PR branch already contained follow-up commit before automerge: fix(doctor): preserve params prototype semantics - PR branch already contained follow-up commit before automerge: fix(sessions): preserve compatible auth overrides Validation: - ClawSweeper review passed for head 64a0739. - Required merge gates passed before the squash merge. Prepared head SHA: 64a0739 Review: #85014 (comment) Co-authored-by: Andy Ye <35905412+TurboTheTurtle@users.noreply.github.com> Co-authored-by: Vincent Koc <vincentkoc@ieee.org> Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com> Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com> Approved-by: takhoffman Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
1 parent 652712e commit b33deb4

5 files changed

Lines changed: 259 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Docs: https://docs.openclaw.ai
1616

1717
### Fixes
1818

19+
- Gateway/sessions: preserve compatible session auth profile overrides when switching models within the same provider, including provider-auth aliases. Fixes #81837. (#81886) Thanks @TurboTheTurtle.
1920
- Providers/Ollama: treat Docker/OrbStack host aliases as local Ollama endpoints so `ollama-local` marker auth works when OpenClaw runs inside a VM/container and Ollama runs on the host. Fixes #84875.
2021
- QA-Lab: keep explicitly searchable/deferred OpenClaw dynamic tool rows report-only by default so tool-coverage gates do not treat mock discovery gaps as hard product failures. (#80319) Thanks @100yenadmin.
2122
- Agents: cap heartbeat model bleed context hints by the stored session window when runtime model metadata is unavailable, so overflow recovery advice does not suggest a larger window than the active session actually has.

src/commands/doctor-legacy-config.migrations.test.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1434,6 +1434,59 @@ describe("normalizeCompatibilityConfigValues", () => {
14341434
]);
14351435
});
14361436

1437+
it("keeps native Ollama params prototype-safe while setting num_ctx", () => {
1438+
const providerParams: Record<string, unknown> = { temperature: 0.2 };
1439+
Object.defineProperty(providerParams, "__proto__", {
1440+
enumerable: true,
1441+
value: { think: "high" },
1442+
});
1443+
const modelParams: Record<string, unknown> = { top_p: 0.9 };
1444+
Object.defineProperty(modelParams, "__proto__", {
1445+
enumerable: true,
1446+
value: { keep_alive: "forever" },
1447+
});
1448+
1449+
const res = normalizeCompatibilityConfigValues({
1450+
models: {
1451+
providers: {
1452+
ollama: {
1453+
baseUrl: "http://localhost:11434",
1454+
api: "ollama",
1455+
contextWindow: 65536,
1456+
params: providerParams,
1457+
models: [
1458+
ollamaModel({
1459+
contextWindow: 32768,
1460+
params: modelParams,
1461+
}),
1462+
],
1463+
},
1464+
},
1465+
},
1466+
});
1467+
1468+
const nextProviderParams = res.config.models?.providers?.ollama?.params as Record<
1469+
string,
1470+
unknown
1471+
>;
1472+
const nextModelParams = res.config.models?.providers?.ollama?.models?.[0]?.params as Record<
1473+
string,
1474+
unknown
1475+
>;
1476+
expect(Object.getPrototypeOf(nextProviderParams)).toBe(Object.prototype);
1477+
expect(Object.getPrototypeOf(nextModelParams)).toBe(Object.prototype);
1478+
expect(Object.getOwnPropertyDescriptor(nextProviderParams, "__proto__")?.value).toEqual({
1479+
think: "high",
1480+
});
1481+
expect(Object.getOwnPropertyDescriptor(nextModelParams, "__proto__")?.value).toEqual({
1482+
keep_alive: "forever",
1483+
});
1484+
expect(nextProviderParams.think).toBeUndefined();
1485+
expect(nextModelParams.keep_alive).toBeUndefined();
1486+
expect(nextProviderParams.num_ctx).toBe(65536);
1487+
expect(nextModelParams.num_ctx).toBe(32768);
1488+
});
1489+
14371490
it("keeps existing provider-level native Ollama params.num_ctx ahead of inherited provider budgets", () => {
14381491
const res = normalizeCompatibilityConfigValues({
14391492
models: {

src/gateway/sessions-patch.test.ts

Lines changed: 148 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { afterEach, describe, expect, test } from "vitest";
2+
import { resetProviderAuthAliasMapCacheForTest } from "../agents/provider-auth-aliases.js";
23
import type { OpenClawConfig } from "../config/config.js";
34
import type { SessionEntry } from "../config/sessions.js";
45
import { createEmptyPluginRegistry } from "../plugins/registry-empty.js";
@@ -108,6 +109,7 @@ function createAllowlistedAnthropicModelCfg(): OpenClawConfig {
108109

109110
describe("gateway sessions patch", () => {
110111
afterEach(() => {
112+
resetProviderAuthAliasMapCacheForTest();
111113
resetPluginRuntimeStateForTest();
112114
});
113115

@@ -271,7 +273,7 @@ describe("gateway sessions patch", () => {
271273
expectPatchError(result, "invalid elevatedLevel");
272274
});
273275

274-
test("clears auth overrides when model patch changes", async () => {
276+
test("preserves same-provider auth overrides when model patch changes", async () => {
275277
const store: Record<string, SessionEntry> = {
276278
"agent:main:main": {
277279
sessionId: "sess",
@@ -294,6 +296,151 @@ describe("gateway sessions patch", () => {
294296
);
295297
expect(entry.providerOverride).toBe("anthropic");
296298
expect(entry.modelOverride).toBe("claude-sonnet-4-6");
299+
expect(entry.authProfileOverride).toBe("anthropic:default");
300+
expect(entry.authProfileOverrideSource).toBe("user");
301+
expect(entry.authProfileOverrideCompactionCount).toBe(3);
302+
});
303+
304+
test("preserves auth overrides for provider-auth aliases when model patch changes", async () => {
305+
const store: Record<string, SessionEntry> = {
306+
"agent:main:main": {
307+
sessionId: "sess-alias",
308+
updatedAt: 1,
309+
providerOverride: "byteplus",
310+
modelOverride: "seedance-1-0-lite-t2v-250428",
311+
authProfileOverride: "byteplus:work",
312+
authProfileOverrideSource: "user",
313+
authProfileOverrideCompactionCount: 2,
314+
} as SessionEntry,
315+
};
316+
const entry = expectPatchOk(
317+
await runPatch({
318+
store,
319+
patch: { key: MAIN_SESSION_KEY, model: "byteplus-plan/ark-code-latest" },
320+
loadGatewayModelCatalog: async () => [
321+
{ provider: "byteplus-plan", id: "ark-code-latest", name: "ark-code-latest" },
322+
],
323+
}),
324+
);
325+
expect(entry.providerOverride).toBe("byteplus-plan");
326+
expect(entry.modelOverride).toBe("ark-code-latest");
327+
expect(entry.authProfileOverride).toBe("byteplus:work");
328+
expect(entry.authProfileOverrideSource).toBe("user");
329+
expect(entry.authProfileOverrideCompactionCount).toBe(2);
330+
});
331+
332+
test("preserves unprefixed auth overrides when existing provider matches model patch", async () => {
333+
const store: Record<string, SessionEntry> = {
334+
"agent:main:main": {
335+
sessionId: "sess-unprefixed-same-provider",
336+
updatedAt: 1,
337+
providerOverride: "anthropic",
338+
modelOverride: "claude-opus-4-6",
339+
authProfileOverride: "work",
340+
authProfileOverrideSource: "user",
341+
authProfileOverrideCompactionCount: 4,
342+
} as SessionEntry,
343+
};
344+
const entry = expectPatchOk(
345+
await runPatch({
346+
store,
347+
patch: { key: MAIN_SESSION_KEY, model: "anthropic/claude-sonnet-4-6" },
348+
loadGatewayModelCatalog: async () => [
349+
{ provider: "anthropic", id: "claude-sonnet-4-6", name: "sonnet" },
350+
],
351+
}),
352+
);
353+
expect(entry.providerOverride).toBe("anthropic");
354+
expect(entry.modelOverride).toBe("claude-sonnet-4-6");
355+
expect(entry.authProfileOverride).toBe("work");
356+
expect(entry.authProfileOverrideSource).toBe("user");
357+
expect(entry.authProfileOverrideCompactionCount).toBe(4);
358+
});
359+
360+
test("preserves unprefixed auth overrides when existing provider is the default", async () => {
361+
const store: Record<string, SessionEntry> = {
362+
"agent:main:main": {
363+
sessionId: "sess-unprefixed-default-provider",
364+
updatedAt: 1,
365+
authProfileOverride: "work",
366+
authProfileOverrideSource: "user",
367+
authProfileOverrideCompactionCount: 4,
368+
} as SessionEntry,
369+
};
370+
const entry = expectPatchOk(
371+
await runPatch({
372+
cfg: {
373+
agents: {
374+
defaults: {
375+
model: { primary: "anthropic/claude-opus-4-6" },
376+
},
377+
},
378+
} as OpenClawConfig,
379+
store,
380+
patch: { key: MAIN_SESSION_KEY, model: "anthropic/claude-sonnet-4-6" },
381+
loadGatewayModelCatalog: async () => [
382+
{ provider: "anthropic", id: "claude-sonnet-4-6", name: "sonnet" },
383+
],
384+
}),
385+
);
386+
expect(entry.providerOverride).toBe("anthropic");
387+
expect(entry.modelOverride).toBe("claude-sonnet-4-6");
388+
expect(entry.authProfileOverride).toBe("work");
389+
expect(entry.authProfileOverrideSource).toBe("user");
390+
expect(entry.authProfileOverrideCompactionCount).toBe(4);
391+
});
392+
393+
test("clears unprefixed auth overrides when model patch changes provider", async () => {
394+
const store: Record<string, SessionEntry> = {
395+
"agent:main:main": {
396+
sessionId: "sess-unprefixed-provider-change",
397+
updatedAt: 1,
398+
providerOverride: "anthropic",
399+
modelOverride: "claude-opus-4-6",
400+
authProfileOverride: "work",
401+
authProfileOverrideSource: "user",
402+
authProfileOverrideCompactionCount: 4,
403+
} as SessionEntry,
404+
};
405+
const entry = expectPatchOk(
406+
await runPatch({
407+
store,
408+
patch: { key: MAIN_SESSION_KEY, model: "openai/gpt-5.4" },
409+
loadGatewayModelCatalog: async () => [
410+
{ provider: "openai", id: "gpt-5.4", name: "gpt-5.4" },
411+
],
412+
}),
413+
);
414+
expect(entry.providerOverride).toBe("openai");
415+
expect(entry.modelOverride).toBe("gpt-5.4");
416+
expect(entry.authProfileOverride).toBeUndefined();
417+
expect(entry.authProfileOverrideSource).toBeUndefined();
418+
expect(entry.authProfileOverrideCompactionCount).toBeUndefined();
419+
});
420+
421+
test("clears provider-prefixed auth overrides when model patch changes provider", async () => {
422+
const store: Record<string, SessionEntry> = {
423+
"agent:main:main": {
424+
sessionId: "sess-provider-change",
425+
updatedAt: 1,
426+
providerOverride: "anthropic",
427+
modelOverride: "claude-opus-4-6",
428+
authProfileOverride: "anthropic:default",
429+
authProfileOverrideSource: "user",
430+
authProfileOverrideCompactionCount: 3,
431+
} as SessionEntry,
432+
};
433+
const entry = expectPatchOk(
434+
await runPatch({
435+
store,
436+
patch: { key: MAIN_SESSION_KEY, model: "openai/gpt-5.4" },
437+
loadGatewayModelCatalog: async () => [
438+
{ provider: "openai", id: "gpt-5.4", name: "gpt-5.4" },
439+
],
440+
}),
441+
);
442+
expect(entry.providerOverride).toBe("openai");
443+
expect(entry.modelOverride).toBe("gpt-5.4");
297444
expect(entry.authProfileOverride).toBeUndefined();
298445
expect(entry.authProfileOverrideSource).toBeUndefined();
299446
expect(entry.authProfileOverrideCompactionCount).toBeUndefined();

src/gateway/sessions-patch.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
resolveDefaultModelForAgent,
1111
resolveSubagentConfiguredModelSelection,
1212
} from "../agents/model-selection.js";
13+
import { resolveProviderIdForAuth } from "../agents/provider-auth-aliases.js";
1314
import { normalizeGroupActivation } from "../auto-reply/group-activation.js";
1415
import {
1516
formatThinkingLevels,
@@ -70,6 +71,43 @@ function normalizeExecAsk(raw: string): "off" | "on-miss" | "always" | undefined
7071
return undefined;
7172
}
7273

74+
function shouldPreserveSessionAuthProfileOverride(params: {
75+
cfg: OpenClawConfig;
76+
entry: SessionEntry;
77+
currentProvider: string;
78+
provider: string;
79+
}): boolean {
80+
const profileOverride = normalizeOptionalString(params.entry.authProfileOverride);
81+
if (!profileOverride) {
82+
return false;
83+
}
84+
const provider = normalizeOptionalLowercaseString(params.provider);
85+
if (!provider) {
86+
return false;
87+
}
88+
const resolvesToTargetProvider = (rawProvider: string | undefined): boolean => {
89+
const candidate = normalizeOptionalLowercaseString(rawProvider);
90+
if (!candidate) {
91+
return false;
92+
}
93+
return (
94+
resolveProviderIdForAuth(candidate, { config: params.cfg }) ===
95+
resolveProviderIdForAuth(provider, { config: params.cfg })
96+
);
97+
};
98+
const delimiterIndex = profileOverride.indexOf(":");
99+
if (delimiterIndex < 0) {
100+
return resolvesToTargetProvider(params.currentProvider);
101+
}
102+
const profileProvider = normalizeOptionalLowercaseString(
103+
profileOverride.slice(0, delimiterIndex),
104+
);
105+
if (!profileProvider) {
106+
return false;
107+
}
108+
return resolvesToTargetProvider(profileProvider);
109+
}
110+
73111
function supportsSpawnLineage(storeKey: string): boolean {
74112
return isSubagentSessionKey(storeKey) || isAcpSessionKey(storeKey);
75113
}
@@ -461,6 +499,12 @@ export async function applySessionsPatchToStore(params: {
461499
model: resolvedDefault.model,
462500
isDefault: true,
463501
},
502+
preserveAuthProfileOverride: shouldPreserveSessionAuthProfileOverride({
503+
cfg,
504+
currentProvider: next.providerOverride ?? next.modelProvider ?? resolvedDefault.provider,
505+
entry: next,
506+
provider: resolvedDefault.provider,
507+
}),
464508
markLiveSwitchPending: true,
465509
});
466510
} else if (raw !== undefined) {
@@ -501,6 +545,12 @@ export async function applySessionsPatchToStore(params: {
501545
model: resolved.ref.model,
502546
isDefault,
503547
},
548+
preserveAuthProfileOverride: shouldPreserveSessionAuthProfileOverride({
549+
cfg,
550+
currentProvider: next.providerOverride ?? next.modelProvider ?? resolvedDefault.provider,
551+
entry: next,
552+
provider: resolved.ref.provider,
553+
}),
504554
markLiveSwitchPending: true,
505555
});
506556
}

test/scripts/mantis-build-telegram-desktop-proof-evidence.test.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,9 @@ describe("scripts/mantis/build-telegram-desktop-proof-evidence", () => {
8282
expect(manifest.artifacts.map((artifact) => artifact.targetPath)).toContain(
8383
"candidate/telegram-desktop-proof.gif",
8484
);
85+
const artifactUrl = "https://github.com/openclaw/openclaw/actions/runs/1/artifacts/2";
8586
const body = renderEvidenceComment({
86-
artifactUrl: "https://github.com/openclaw/openclaw/actions/runs/1/artifacts/2",
87+
artifactUrl,
8788
manifest,
8889
marker: "<!-- mantis-telegram-desktop-proof -->",
8990
rawBase: "https://qa.openclaw.ai/mantis/telegram-desktop/pr-1/run-1",
@@ -92,9 +93,13 @@ describe("scripts/mantis/build-telegram-desktop-proof-evidence", () => {
9293
treeUrl: "https://qa.openclaw.ai/mantis/telegram-desktop/pr-1/run-1/index.json",
9394
});
9495

96+
expect(body).toContain("<!-- mantis-telegram-desktop-proof -->");
97+
expect(body).toContain("## Mantis Telegram Desktop Proof");
98+
expect(body).toContain("- Baseline: `pass` at `aaa`, expected baseline visual proof captured");
9599
expect(body).toContain(
96-
"- Artifact: https://github.com/openclaw/openclaw/actions/runs/1/artifacts/2",
100+
"- Candidate: `pass` at `bbb`, expected candidate visual proof captured",
97101
);
102+
expect(body).toContain(`- Artifact: ${artifactUrl}`);
98103
expect(body).toContain('<table width="100%">');
99104
expect(body).toContain(
100105
'<img src="https://qa.openclaw.ai/mantis/telegram-desktop/pr-1/run-1/baseline/telegram-desktop-proof.gif" width="100%"',

0 commit comments

Comments
 (0)