fix(models): preserve @YYYYMMDD version suffixes (#48896) thanks @Alix-007
Co-authored-by: Alix-007 <267018309+Alix-007@users.noreply.github.com> Co-authored-by: frankekn <frank.ekn@gmail.com>
This commit is contained in:
parent
4ca87fa4b0
commit
2c579b6ac1
@ -126,6 +126,10 @@ Docs: https://docs.openclaw.ai
|
||||
- Telegram/network: preserve sticky IPv4 fallback state across polling restarts so hosts with unstable IPv6 to `api.telegram.org` stop re-triggering repeated Telegram timeouts after each restart. (#48282) Thanks @yassinebkr.
|
||||
- Plugins/subagents: forward per-run provider and model overrides through gateway plugin subagent dispatch so plugin-launched agent delegations honor explicit model selection again. (#48277) Thanks @jalehman.
|
||||
- Agents/compaction: write minimal boundary summaries for empty preparations while keeping split-turn prefixes on the normal path, so no-summarizable-message sessions stop retriggering the safeguard loop. (#42215) thanks @lml2468.
|
||||
- Models/chat commands: keep `/model ...@YYYYMMDD` version suffixes intact by default, but still honor matching stored numeric auth-profile overrides for the same provider. (#48896) Thanks @Alix-007.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Agents/bootstrap warnings: move bootstrap truncation warnings out of the system prompt and into the per-turn prompt body so prompt-cache reuse stays stable when truncation warnings appear or disappear. (#48753) Thanks @scoootscooob and @obviyus.
|
||||
- Telegram/DM topic session keys: route named-account DM topics through the same per-account base session key across inbound messages, native commands, and session-state lookups so `/status` and thread recovery stop creating phantom `agent:main:main:thread:...` sessions. (#48204) Thanks @vincentkoc.
|
||||
- macOS/node service startup: use `openclaw node start/stop --json` from the Mac app instead of the removed `openclaw service node ...` command shape, so current CLI installs expose the full node exec surface again. (#46843) Fixes #43171. Thanks @Br1an67.
|
||||
|
||||
@ -53,4 +53,17 @@ describe("splitTrailingAuthProfile", () => {
|
||||
profile: "google-gemini-cli:test@gmail.com",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps @YYYYMMDD version suffixes in model ids", () => {
|
||||
expect(splitTrailingAuthProfile("custom/vertex-ai_claude-haiku-4-5@20251001")).toEqual({
|
||||
model: "custom/vertex-ai_claude-haiku-4-5@20251001",
|
||||
});
|
||||
});
|
||||
|
||||
it("supports auth profiles after @YYYYMMDD version suffixes", () => {
|
||||
expect(splitTrailingAuthProfile("custom/vertex-ai_claude-haiku-4-5@20251001@work")).toEqual({
|
||||
model: "custom/vertex-ai_claude-haiku-4-5@20251001",
|
||||
profile: "work",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -8,11 +8,20 @@ export function splitTrailingAuthProfile(raw: string): {
|
||||
}
|
||||
|
||||
const lastSlash = trimmed.lastIndexOf("/");
|
||||
const profileDelimiter = trimmed.indexOf("@", lastSlash + 1);
|
||||
let profileDelimiter = trimmed.indexOf("@", lastSlash + 1);
|
||||
if (profileDelimiter <= 0) {
|
||||
return { model: trimmed };
|
||||
}
|
||||
|
||||
const versionSuffix = trimmed.slice(profileDelimiter + 1);
|
||||
if (/^\d{8}(?:@|$)/.test(versionSuffix)) {
|
||||
const nextDelimiter = trimmed.indexOf("@", profileDelimiter + 9);
|
||||
if (nextDelimiter < 0) {
|
||||
return { model: trimmed };
|
||||
}
|
||||
profileDelimiter = nextDelimiter;
|
||||
}
|
||||
|
||||
const model = trimmed.slice(0, profileDelimiter).trim();
|
||||
const profile = trimmed.slice(profileDelimiter + 1).trim();
|
||||
if (!model || !profile) {
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
clearRuntimeAuthProfileStoreSnapshots,
|
||||
replaceRuntimeAuthProfileStoreSnapshots,
|
||||
} from "../../agents/auth-profiles.js";
|
||||
import type { ModelAliasIndex } from "../../agents/model-selection.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import type { SessionEntry } from "../../config/sessions.js";
|
||||
@ -8,6 +12,7 @@ import {
|
||||
maybeHandleModelDirectiveInfo,
|
||||
resolveModelSelectionFromDirective,
|
||||
} from "./directive-handling.model.js";
|
||||
import { persistInlineDirectives } from "./directive-handling.persist.js";
|
||||
|
||||
// Mock dependencies for directive handling persistence.
|
||||
vi.mock("../../agents/agent-scope.js", () => ({
|
||||
@ -28,6 +33,8 @@ vi.mock("../../infra/system-events.js", () => ({
|
||||
enqueueSystemEvent: vi.fn(),
|
||||
}));
|
||||
|
||||
const TEST_AGENT_DIR = "/tmp/agent";
|
||||
|
||||
function baseAliasIndex(): ModelAliasIndex {
|
||||
return { byAlias: new Map(), byKey: new Map() };
|
||||
}
|
||||
@ -39,6 +46,31 @@ function baseConfig(): OpenClawConfig {
|
||||
} as unknown as OpenClawConfig;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
clearRuntimeAuthProfileStoreSnapshots();
|
||||
replaceRuntimeAuthProfileStoreSnapshots([
|
||||
{
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
store: { version: 1, profiles: {} },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
clearRuntimeAuthProfileStoreSnapshots();
|
||||
});
|
||||
|
||||
function setAuthProfiles(
|
||||
profiles: Record<string, { type: "api_key"; provider: string; key: string }>,
|
||||
) {
|
||||
replaceRuntimeAuthProfileStoreSnapshots([
|
||||
{
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
store: { version: 1, profiles },
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
function resolveModelSelectionForCommand(params: {
|
||||
command: string;
|
||||
allowedModelKeys: Set<string>;
|
||||
@ -47,7 +79,7 @@ function resolveModelSelectionForCommand(params: {
|
||||
return resolveModelSelectionFromDirective({
|
||||
directives: parseInlineDirectives(params.command),
|
||||
cfg: { commands: { text: true } } as unknown as OpenClawConfig,
|
||||
agentDir: "/tmp/agent",
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
defaultProvider: "anthropic",
|
||||
defaultModel: "claude-opus-4-5",
|
||||
aliasIndex: baseAliasIndex(),
|
||||
@ -63,7 +95,7 @@ async function resolveModelInfoReply(
|
||||
return maybeHandleModelDirectiveInfo({
|
||||
directives: parseInlineDirectives("/model"),
|
||||
cfg: baseConfig(),
|
||||
agentDir: "/tmp/agent",
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
activeAgentId: "main",
|
||||
provider: "anthropic",
|
||||
model: "claude-opus-4-5",
|
||||
@ -181,6 +213,342 @@ describe("/model chat UX", () => {
|
||||
isDefault: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("treats @YYYYMMDD as a profile override when that profile exists for the resolved provider", () => {
|
||||
setAuthProfiles({
|
||||
"20251001": {
|
||||
type: "api_key",
|
||||
provider: "openai",
|
||||
key: "sk-test",
|
||||
},
|
||||
});
|
||||
|
||||
const resolved = resolveModelSelectionForCommand({
|
||||
command: "/model openai/gpt-4o@20251001",
|
||||
allowedModelKeys: new Set(["openai/gpt-4o"]),
|
||||
allowedModelCatalog: [],
|
||||
});
|
||||
|
||||
expect(resolved.errorText).toBeUndefined();
|
||||
expect(resolved.modelSelection).toEqual({
|
||||
provider: "openai",
|
||||
model: "gpt-4o",
|
||||
isDefault: false,
|
||||
});
|
||||
expect(resolved.profileOverride).toBe("20251001");
|
||||
});
|
||||
|
||||
it("supports alias selections with numeric auth-profile overrides", () => {
|
||||
setAuthProfiles({
|
||||
"20251001": {
|
||||
type: "api_key",
|
||||
provider: "openai",
|
||||
key: "sk-test",
|
||||
},
|
||||
});
|
||||
|
||||
const aliasIndex: ModelAliasIndex = {
|
||||
byAlias: new Map([["gpt", { alias: "gpt", ref: { provider: "openai", model: "gpt-4o" } }]]),
|
||||
byKey: new Map([["openai/gpt-4o", ["gpt"]]]),
|
||||
};
|
||||
|
||||
const resolved = resolveModelSelectionFromDirective({
|
||||
directives: parseInlineDirectives("/model gpt@20251001"),
|
||||
cfg: { commands: { text: true } } as unknown as OpenClawConfig,
|
||||
agentDir: "/tmp/agent",
|
||||
defaultProvider: "anthropic",
|
||||
defaultModel: "claude-opus-4-5",
|
||||
aliasIndex,
|
||||
allowedModelKeys: new Set(["openai/gpt-4o"]),
|
||||
allowedModelCatalog: [],
|
||||
provider: "anthropic",
|
||||
});
|
||||
|
||||
expect(resolved.errorText).toBeUndefined();
|
||||
expect(resolved.modelSelection).toEqual({
|
||||
provider: "openai",
|
||||
model: "gpt-4o",
|
||||
isDefault: false,
|
||||
alias: "gpt",
|
||||
});
|
||||
expect(resolved.profileOverride).toBe("20251001");
|
||||
});
|
||||
|
||||
it("supports providerless allowlist selections with numeric auth-profile overrides", () => {
|
||||
setAuthProfiles({
|
||||
"20251001": {
|
||||
type: "api_key",
|
||||
provider: "openai",
|
||||
key: "sk-test",
|
||||
},
|
||||
});
|
||||
|
||||
const resolved = resolveModelSelectionForCommand({
|
||||
command: "/model gpt-4o@20251001",
|
||||
allowedModelKeys: new Set(["openai/gpt-4o"]),
|
||||
allowedModelCatalog: [],
|
||||
});
|
||||
|
||||
expect(resolved.errorText).toBeUndefined();
|
||||
expect(resolved.modelSelection).toEqual({
|
||||
provider: "openai",
|
||||
model: "gpt-4o",
|
||||
isDefault: false,
|
||||
});
|
||||
expect(resolved.profileOverride).toBe("20251001");
|
||||
});
|
||||
|
||||
it("keeps @YYYYMMDD as part of the model when the stored numeric profile is for another provider", () => {
|
||||
setAuthProfiles({
|
||||
"20251001": {
|
||||
type: "api_key",
|
||||
provider: "anthropic",
|
||||
key: "sk-test",
|
||||
},
|
||||
});
|
||||
|
||||
const resolved = resolveModelSelectionForCommand({
|
||||
command: "/model custom/vertex-ai_claude-haiku-4-5@20251001",
|
||||
allowedModelKeys: new Set(["custom/vertex-ai_claude-haiku-4-5@20251001"]),
|
||||
allowedModelCatalog: [],
|
||||
});
|
||||
|
||||
expect(resolved.errorText).toBeUndefined();
|
||||
expect(resolved.modelSelection).toEqual({
|
||||
provider: "custom",
|
||||
model: "vertex-ai_claude-haiku-4-5@20251001",
|
||||
isDefault: false,
|
||||
});
|
||||
expect(resolved.profileOverride).toBeUndefined();
|
||||
});
|
||||
|
||||
it("persists inferred numeric auth-profile overrides for mixed-content messages", async () => {
|
||||
setAuthProfiles({
|
||||
"20251001": {
|
||||
type: "api_key",
|
||||
provider: "openai",
|
||||
key: "sk-test",
|
||||
},
|
||||
});
|
||||
|
||||
const directives = parseInlineDirectives("/model openai/gpt-4o@20251001 hello");
|
||||
const sessionEntry = {
|
||||
sessionId: "s1",
|
||||
updatedAt: Date.now(),
|
||||
} as SessionEntry;
|
||||
const sessionStore = { "agent:main:dm:1": sessionEntry };
|
||||
|
||||
await persistInlineDirectives({
|
||||
directives,
|
||||
effectiveModelDirective: directives.rawModelDirective,
|
||||
cfg: baseConfig(),
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
sessionEntry,
|
||||
sessionStore,
|
||||
sessionKey: "agent:main:dm:1",
|
||||
storePath: undefined,
|
||||
elevatedEnabled: false,
|
||||
elevatedAllowed: false,
|
||||
defaultProvider: "anthropic",
|
||||
defaultModel: "claude-opus-4-5",
|
||||
aliasIndex: baseAliasIndex(),
|
||||
allowedModelKeys: new Set(["openai/gpt-4o", "openai/gpt-4o@20251001"]),
|
||||
provider: "anthropic",
|
||||
model: "claude-opus-4-5",
|
||||
initialModelLabel: "anthropic/claude-opus-4-5",
|
||||
formatModelSwitchEvent: (label) => label,
|
||||
agentCfg: baseConfig().agents?.defaults,
|
||||
});
|
||||
|
||||
expect(sessionEntry.providerOverride).toBe("openai");
|
||||
expect(sessionEntry.modelOverride).toBe("gpt-4o");
|
||||
expect(sessionEntry.authProfileOverride).toBe("20251001");
|
||||
});
|
||||
|
||||
it("persists alias-based numeric auth-profile overrides for mixed-content messages", async () => {
|
||||
setAuthProfiles({
|
||||
"20251001": {
|
||||
type: "api_key",
|
||||
provider: "openai",
|
||||
key: "sk-test",
|
||||
},
|
||||
});
|
||||
|
||||
const aliasIndex: ModelAliasIndex = {
|
||||
byAlias: new Map([["gpt", { alias: "gpt", ref: { provider: "openai", model: "gpt-4o" } }]]),
|
||||
byKey: new Map([["openai/gpt-4o", ["gpt"]]]),
|
||||
};
|
||||
const directives = parseInlineDirectives("/model gpt@20251001 hello");
|
||||
const sessionEntry = {
|
||||
sessionId: "s1",
|
||||
updatedAt: Date.now(),
|
||||
} as SessionEntry;
|
||||
const sessionStore = { "agent:main:dm:1": sessionEntry };
|
||||
|
||||
await persistInlineDirectives({
|
||||
directives,
|
||||
effectiveModelDirective: directives.rawModelDirective,
|
||||
cfg: baseConfig(),
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
sessionEntry,
|
||||
sessionStore,
|
||||
sessionKey: "agent:main:dm:1",
|
||||
storePath: undefined,
|
||||
elevatedEnabled: false,
|
||||
elevatedAllowed: false,
|
||||
defaultProvider: "anthropic",
|
||||
defaultModel: "claude-opus-4-5",
|
||||
aliasIndex,
|
||||
allowedModelKeys: new Set(["openai/gpt-4o"]),
|
||||
provider: "anthropic",
|
||||
model: "claude-opus-4-5",
|
||||
initialModelLabel: "anthropic/claude-opus-4-5",
|
||||
formatModelSwitchEvent: (label) => label,
|
||||
agentCfg: baseConfig().agents?.defaults,
|
||||
});
|
||||
|
||||
expect(sessionEntry.providerOverride).toBe("openai");
|
||||
expect(sessionEntry.modelOverride).toBe("gpt-4o");
|
||||
expect(sessionEntry.authProfileOverride).toBe("20251001");
|
||||
});
|
||||
|
||||
it("persists providerless numeric auth-profile overrides for mixed-content messages", async () => {
|
||||
setAuthProfiles({
|
||||
"20251001": {
|
||||
type: "api_key",
|
||||
provider: "openai",
|
||||
key: "sk-test",
|
||||
},
|
||||
});
|
||||
|
||||
const directives = parseInlineDirectives("/model gpt-4o@20251001 hello");
|
||||
const sessionEntry = {
|
||||
sessionId: "s1",
|
||||
updatedAt: Date.now(),
|
||||
} as SessionEntry;
|
||||
const sessionStore = { "agent:main:dm:1": sessionEntry };
|
||||
|
||||
await persistInlineDirectives({
|
||||
directives,
|
||||
effectiveModelDirective: directives.rawModelDirective,
|
||||
cfg: baseConfig(),
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
sessionEntry,
|
||||
sessionStore,
|
||||
sessionKey: "agent:main:dm:1",
|
||||
storePath: undefined,
|
||||
elevatedEnabled: false,
|
||||
elevatedAllowed: false,
|
||||
defaultProvider: "anthropic",
|
||||
defaultModel: "claude-opus-4-5",
|
||||
aliasIndex: baseAliasIndex(),
|
||||
allowedModelKeys: new Set(["openai/gpt-4o"]),
|
||||
provider: "anthropic",
|
||||
model: "claude-opus-4-5",
|
||||
initialModelLabel: "anthropic/claude-opus-4-5",
|
||||
formatModelSwitchEvent: (label) => label,
|
||||
agentCfg: baseConfig().agents?.defaults,
|
||||
});
|
||||
|
||||
expect(sessionEntry.providerOverride).toBe("openai");
|
||||
expect(sessionEntry.modelOverride).toBe("gpt-4o");
|
||||
expect(sessionEntry.authProfileOverride).toBe("20251001");
|
||||
});
|
||||
|
||||
it("persists explicit auth profiles after @YYYYMMDD version suffixes in mixed-content messages", async () => {
|
||||
setAuthProfiles({
|
||||
work: {
|
||||
type: "api_key",
|
||||
provider: "custom",
|
||||
key: "sk-test",
|
||||
},
|
||||
});
|
||||
|
||||
const directives = parseInlineDirectives(
|
||||
"/model custom/vertex-ai_claude-haiku-4-5@20251001@work hello",
|
||||
);
|
||||
const sessionEntry = {
|
||||
sessionId: "s1",
|
||||
updatedAt: Date.now(),
|
||||
} as SessionEntry;
|
||||
const sessionStore = { "agent:main:dm:1": sessionEntry };
|
||||
|
||||
await persistInlineDirectives({
|
||||
directives,
|
||||
effectiveModelDirective: directives.rawModelDirective,
|
||||
cfg: baseConfig(),
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
sessionEntry,
|
||||
sessionStore,
|
||||
sessionKey: "agent:main:dm:1",
|
||||
storePath: undefined,
|
||||
elevatedEnabled: false,
|
||||
elevatedAllowed: false,
|
||||
defaultProvider: "anthropic",
|
||||
defaultModel: "claude-opus-4-5",
|
||||
aliasIndex: baseAliasIndex(),
|
||||
allowedModelKeys: new Set(["custom/vertex-ai_claude-haiku-4-5@20251001"]),
|
||||
provider: "anthropic",
|
||||
model: "claude-opus-4-5",
|
||||
initialModelLabel: "anthropic/claude-opus-4-5",
|
||||
formatModelSwitchEvent: (label) => label,
|
||||
agentCfg: baseConfig().agents?.defaults,
|
||||
});
|
||||
|
||||
expect(sessionEntry.providerOverride).toBe("custom");
|
||||
expect(sessionEntry.modelOverride).toBe("vertex-ai_claude-haiku-4-5@20251001");
|
||||
expect(sessionEntry.authProfileOverride).toBe("work");
|
||||
});
|
||||
|
||||
it("ignores invalid mixed-content model directives during persistence", async () => {
|
||||
setAuthProfiles({
|
||||
"20251001": {
|
||||
type: "api_key",
|
||||
provider: "openai",
|
||||
key: "sk-test",
|
||||
},
|
||||
});
|
||||
|
||||
const directives = parseInlineDirectives("/model 99 hello");
|
||||
const sessionEntry = {
|
||||
sessionId: "s1",
|
||||
updatedAt: Date.now(),
|
||||
providerOverride: "openai",
|
||||
modelOverride: "gpt-4o",
|
||||
authProfileOverride: "20251001",
|
||||
authProfileOverrideSource: "user",
|
||||
} as SessionEntry;
|
||||
const sessionStore = { "agent:main:dm:1": sessionEntry };
|
||||
|
||||
const persisted = await persistInlineDirectives({
|
||||
directives,
|
||||
effectiveModelDirective: directives.rawModelDirective,
|
||||
cfg: baseConfig(),
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
sessionEntry,
|
||||
sessionStore,
|
||||
sessionKey: "agent:main:dm:1",
|
||||
storePath: undefined,
|
||||
elevatedEnabled: false,
|
||||
elevatedAllowed: false,
|
||||
defaultProvider: "anthropic",
|
||||
defaultModel: "claude-opus-4-5",
|
||||
aliasIndex: baseAliasIndex(),
|
||||
allowedModelKeys: new Set(["openai/gpt-4o"]),
|
||||
provider: "openai",
|
||||
model: "gpt-4o",
|
||||
initialModelLabel: "openai/gpt-4o",
|
||||
formatModelSwitchEvent: (label) => label,
|
||||
agentCfg: baseConfig().agents?.defaults,
|
||||
});
|
||||
|
||||
expect(persisted.provider).toBe("openai");
|
||||
expect(persisted.model).toBe("gpt-4o");
|
||||
expect(sessionEntry.providerOverride).toBe("openai");
|
||||
expect(sessionEntry.modelOverride).toBe("gpt-4o");
|
||||
expect(sessionEntry.authProfileOverride).toBe("20251001");
|
||||
expect(sessionEntry.authProfileOverrideSource).toBe("user");
|
||||
});
|
||||
});
|
||||
|
||||
describe("handleDirectiveOnly model persist behavior (fixes #1435)", () => {
|
||||
|
||||
@ -1,8 +1,12 @@
|
||||
import { resolveAuthStorePathForDisplay } from "../../agents/auth-profiles.js";
|
||||
import {
|
||||
ensureAuthProfileStore,
|
||||
resolveAuthStorePathForDisplay,
|
||||
} from "../../agents/auth-profiles.js";
|
||||
import {
|
||||
type ModelAliasIndex,
|
||||
modelKey,
|
||||
normalizeProviderId,
|
||||
normalizeProviderIdForAuth,
|
||||
resolveConfiguredModelRef,
|
||||
resolveModelRefFromString,
|
||||
} from "../../agents/model-selection.js";
|
||||
@ -353,6 +357,39 @@ export async function maybeHandleModelDirectiveInfo(params: {
|
||||
return { text: lines.join("\n") };
|
||||
}
|
||||
|
||||
function resolveStoredNumericProfileModelDirective(params: { raw: string; agentDir: string }): {
|
||||
modelRaw: string;
|
||||
profileId: string;
|
||||
profileProvider: string;
|
||||
} | null {
|
||||
const trimmed = params.raw.trim();
|
||||
const lastSlash = trimmed.lastIndexOf("/");
|
||||
const profileDelimiter = trimmed.indexOf("@", lastSlash + 1);
|
||||
if (profileDelimiter <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const profileId = trimmed.slice(profileDelimiter + 1).trim();
|
||||
if (!/^\d{8}$/.test(profileId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const modelRaw = trimmed.slice(0, profileDelimiter).trim();
|
||||
if (!modelRaw) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const store = ensureAuthProfileStore(params.agentDir, {
|
||||
allowKeychainPrompt: false,
|
||||
});
|
||||
const profile = store.profiles[profileId];
|
||||
if (!profile) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { modelRaw, profileId, profileProvider: profile.provider };
|
||||
}
|
||||
|
||||
export function resolveModelSelectionFromDirective(params: {
|
||||
directives: InlineDirectives;
|
||||
cfg: OpenClawConfig;
|
||||
@ -376,6 +413,28 @@ export function resolveModelSelectionFromDirective(params: {
|
||||
}
|
||||
|
||||
const raw = params.directives.rawModelDirective.trim();
|
||||
const storedNumericProfile =
|
||||
params.directives.rawModelProfile === undefined
|
||||
? resolveStoredNumericProfileModelDirective({
|
||||
raw,
|
||||
agentDir: params.agentDir,
|
||||
})
|
||||
: null;
|
||||
const storedNumericProfileSelection = storedNumericProfile
|
||||
? resolveModelDirectiveSelection({
|
||||
raw: storedNumericProfile.modelRaw,
|
||||
defaultProvider: params.defaultProvider,
|
||||
defaultModel: params.defaultModel,
|
||||
aliasIndex: params.aliasIndex,
|
||||
allowedModelKeys: params.allowedModelKeys,
|
||||
})
|
||||
: null;
|
||||
const useStoredNumericProfile =
|
||||
Boolean(storedNumericProfileSelection?.selection) &&
|
||||
normalizeProviderIdForAuth(storedNumericProfileSelection?.selection?.provider ?? "") ===
|
||||
normalizeProviderIdForAuth(storedNumericProfile?.profileProvider ?? "");
|
||||
const modelRaw =
|
||||
useStoredNumericProfile && storedNumericProfile ? storedNumericProfile.modelRaw : raw;
|
||||
let modelSelection: ModelDirectiveSelection | undefined;
|
||||
|
||||
if (/^[0-9]+$/.test(raw)) {
|
||||
@ -390,7 +449,7 @@ export function resolveModelSelectionFromDirective(params: {
|
||||
}
|
||||
|
||||
const explicit = resolveModelRefFromString({
|
||||
raw,
|
||||
raw: modelRaw,
|
||||
defaultProvider: params.defaultProvider,
|
||||
aliasIndex: params.aliasIndex,
|
||||
});
|
||||
@ -410,7 +469,7 @@ export function resolveModelSelectionFromDirective(params: {
|
||||
|
||||
if (!modelSelection) {
|
||||
const resolved = resolveModelDirectiveSelection({
|
||||
raw,
|
||||
raw: modelRaw,
|
||||
defaultProvider: params.defaultProvider,
|
||||
defaultModel: params.defaultModel,
|
||||
aliasIndex: params.aliasIndex,
|
||||
@ -427,9 +486,12 @@ export function resolveModelSelectionFromDirective(params: {
|
||||
}
|
||||
|
||||
let profileOverride: string | undefined;
|
||||
if (modelSelection && params.directives.rawModelProfile) {
|
||||
const rawProfile =
|
||||
params.directives.rawModelProfile ??
|
||||
(useStoredNumericProfile ? storedNumericProfile?.profileId : undefined);
|
||||
if (modelSelection && rawProfile) {
|
||||
const profileResolved = resolveProfileOverride({
|
||||
rawProfile: params.directives.rawModelProfile,
|
||||
rawProfile,
|
||||
provider: modelSelection.provider,
|
||||
cfg: params.cfg,
|
||||
agentDir: params.agentDir,
|
||||
|
||||
@ -8,16 +8,14 @@ import { DEFAULT_CONTEXT_TOKENS } from "../../agents/defaults.js";
|
||||
import {
|
||||
buildModelAliasIndex,
|
||||
type ModelAliasIndex,
|
||||
modelKey,
|
||||
resolveDefaultModelForAgent,
|
||||
resolveModelRefFromString,
|
||||
} from "../../agents/model-selection.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { type SessionEntry, updateSessionStore } from "../../config/sessions.js";
|
||||
import { enqueueSystemEvent } from "../../infra/system-events.js";
|
||||
import { applyVerboseOverride } from "../../sessions/level-overrides.js";
|
||||
import { applyModelOverrideToSessionEntry } from "../../sessions/model-overrides.js";
|
||||
import { resolveProfileOverride } from "./directive-handling.auth.js";
|
||||
import { resolveModelSelectionFromDirective } from "./directive-handling.model.js";
|
||||
import type { InlineDirectives } from "./directive-handling.parse.js";
|
||||
import { enqueueModeSwitchEvents } from "./directive-handling.shared.js";
|
||||
import type { ElevatedLevel, ReasoningLevel } from "./directives.js";
|
||||
@ -64,7 +62,7 @@ export async function persistInlineDirectives(params: {
|
||||
const activeAgentId = sessionKey
|
||||
? resolveSessionAgentId({ sessionKey, config: cfg })
|
||||
: resolveDefaultAgentId(cfg);
|
||||
const agentDir = resolveAgentDir(cfg, activeAgentId);
|
||||
const agentDir = params.agentDir ?? resolveAgentDir(cfg, activeAgentId);
|
||||
|
||||
if (sessionEntry && sessionStore && sessionKey) {
|
||||
const prevElevatedLevel =
|
||||
@ -139,49 +137,40 @@ export async function persistInlineDirectives(params: {
|
||||
? params.effectiveModelDirective
|
||||
: undefined;
|
||||
if (modelDirective) {
|
||||
const resolved = resolveModelRefFromString({
|
||||
raw: modelDirective,
|
||||
const modelResolution = resolveModelSelectionFromDirective({
|
||||
directives: {
|
||||
...directives,
|
||||
hasModelDirective: true,
|
||||
rawModelDirective: modelDirective,
|
||||
},
|
||||
cfg,
|
||||
agentDir,
|
||||
defaultProvider,
|
||||
defaultModel,
|
||||
aliasIndex,
|
||||
allowedModelKeys,
|
||||
allowedModelCatalog: [],
|
||||
provider,
|
||||
});
|
||||
if (resolved) {
|
||||
const key = modelKey(resolved.ref.provider, resolved.ref.model);
|
||||
if (allowedModelKeys.size === 0 || allowedModelKeys.has(key)) {
|
||||
let profileOverride: string | undefined;
|
||||
if (directives.rawModelProfile) {
|
||||
const profileResolved = resolveProfileOverride({
|
||||
rawProfile: directives.rawModelProfile,
|
||||
provider: resolved.ref.provider,
|
||||
cfg,
|
||||
agentDir,
|
||||
});
|
||||
if (profileResolved.error) {
|
||||
throw new Error(profileResolved.error);
|
||||
}
|
||||
profileOverride = profileResolved.profileId;
|
||||
}
|
||||
const isDefault =
|
||||
resolved.ref.provider === defaultProvider && resolved.ref.model === defaultModel;
|
||||
const { updated: modelUpdated } = applyModelOverrideToSessionEntry({
|
||||
entry: sessionEntry,
|
||||
selection: {
|
||||
provider: resolved.ref.provider,
|
||||
model: resolved.ref.model,
|
||||
isDefault,
|
||||
},
|
||||
profileOverride,
|
||||
});
|
||||
provider = resolved.ref.provider;
|
||||
model = resolved.ref.model;
|
||||
const nextLabel = `${provider}/${model}`;
|
||||
if (nextLabel !== initialModelLabel) {
|
||||
enqueueSystemEvent(formatModelSwitchEvent(nextLabel, resolved.alias), {
|
||||
if (modelResolution.modelSelection) {
|
||||
const { updated: modelUpdated } = applyModelOverrideToSessionEntry({
|
||||
entry: sessionEntry,
|
||||
selection: modelResolution.modelSelection,
|
||||
profileOverride: modelResolution.profileOverride,
|
||||
});
|
||||
provider = modelResolution.modelSelection.provider;
|
||||
model = modelResolution.modelSelection.model;
|
||||
const nextLabel = `${provider}/${model}`;
|
||||
if (nextLabel !== initialModelLabel) {
|
||||
enqueueSystemEvent(
|
||||
formatModelSwitchEvent(nextLabel, modelResolution.modelSelection.alias),
|
||||
{
|
||||
sessionKey,
|
||||
contextKey: `model:${nextLabel}`,
|
||||
});
|
||||
}
|
||||
updated = updated || modelUpdated;
|
||||
},
|
||||
);
|
||||
}
|
||||
updated = updated || modelUpdated;
|
||||
}
|
||||
}
|
||||
if (directives.hasQueueDirective && directives.queueReset) {
|
||||
|
||||
@ -1428,6 +1428,63 @@ describe("initSessionState preserves behavior overrides across /new and /reset",
|
||||
}
|
||||
});
|
||||
|
||||
it("preserves selected auth profile overrides across /new and /reset", async () => {
|
||||
const storePath = await createStorePath("openclaw-reset-model-auth-");
|
||||
const sessionKey = "agent:main:telegram:dm:user-model-auth";
|
||||
const existingSessionId = "existing-session-model-auth";
|
||||
const overrides = {
|
||||
providerOverride: "openai",
|
||||
modelOverride: "gpt-4o",
|
||||
authProfileOverride: "20251001",
|
||||
authProfileOverrideSource: "user",
|
||||
authProfileOverrideCompactionCount: 2,
|
||||
} as const;
|
||||
const cases = [
|
||||
{
|
||||
name: "new preserves selected auth profile overrides",
|
||||
body: "/new",
|
||||
},
|
||||
{
|
||||
name: "reset preserves selected auth profile overrides",
|
||||
body: "/reset",
|
||||
},
|
||||
] as const;
|
||||
|
||||
for (const testCase of cases) {
|
||||
await seedSessionStoreWithOverrides({
|
||||
storePath,
|
||||
sessionKey,
|
||||
sessionId: existingSessionId,
|
||||
overrides: { ...overrides },
|
||||
});
|
||||
|
||||
const cfg = {
|
||||
session: { store: storePath, idleMinutes: 999 },
|
||||
} as OpenClawConfig;
|
||||
|
||||
const result = await initSessionState({
|
||||
ctx: {
|
||||
Body: testCase.body,
|
||||
RawBody: testCase.body,
|
||||
CommandBody: testCase.body,
|
||||
From: "user-model-auth",
|
||||
To: "bot",
|
||||
ChatType: "direct",
|
||||
SessionKey: sessionKey,
|
||||
Provider: "telegram",
|
||||
Surface: "telegram",
|
||||
},
|
||||
cfg,
|
||||
commandAuthorized: true,
|
||||
});
|
||||
|
||||
expect(result.isNewSession, testCase.name).toBe(true);
|
||||
expect(result.resetTriggered, testCase.name).toBe(true);
|
||||
expect(result.sessionId, testCase.name).not.toBe(existingSessionId);
|
||||
expect(result.sessionEntry, testCase.name).toMatchObject(overrides);
|
||||
}
|
||||
});
|
||||
|
||||
it("archives the old session store entry on /new", async () => {
|
||||
const storePath = await createStorePath("openclaw-archive-old-");
|
||||
const sessionKey = "agent:main:telegram:dm:user-archive";
|
||||
|
||||
@ -217,6 +217,9 @@ export async function initSessionState(params: {
|
||||
let persistedTtsAuto: TtsAutoMode | undefined;
|
||||
let persistedModelOverride: string | undefined;
|
||||
let persistedProviderOverride: string | undefined;
|
||||
let persistedAuthProfileOverride: string | undefined;
|
||||
let persistedAuthProfileOverrideSource: SessionEntry["authProfileOverrideSource"];
|
||||
let persistedAuthProfileOverrideCompactionCount: number | undefined;
|
||||
let persistedLabel: string | undefined;
|
||||
|
||||
const normalizedChatType = normalizeChatType(ctx.ChatType);
|
||||
@ -353,6 +356,9 @@ export async function initSessionState(params: {
|
||||
persistedTtsAuto = entry.ttsAuto;
|
||||
persistedModelOverride = entry.modelOverride;
|
||||
persistedProviderOverride = entry.providerOverride;
|
||||
persistedAuthProfileOverride = entry.authProfileOverride;
|
||||
persistedAuthProfileOverrideSource = entry.authProfileOverrideSource;
|
||||
persistedAuthProfileOverrideCompactionCount = entry.authProfileOverrideCompactionCount;
|
||||
persistedLabel = entry.label;
|
||||
} else {
|
||||
sessionId = crypto.randomUUID();
|
||||
@ -369,6 +375,9 @@ export async function initSessionState(params: {
|
||||
persistedTtsAuto = entry.ttsAuto;
|
||||
persistedModelOverride = entry.modelOverride;
|
||||
persistedProviderOverride = entry.providerOverride;
|
||||
persistedAuthProfileOverride = entry.authProfileOverride;
|
||||
persistedAuthProfileOverrideSource = entry.authProfileOverrideSource;
|
||||
persistedAuthProfileOverrideCompactionCount = entry.authProfileOverrideCompactionCount;
|
||||
persistedLabel = entry.label;
|
||||
}
|
||||
}
|
||||
@ -420,6 +429,11 @@ export async function initSessionState(params: {
|
||||
responseUsage: baseEntry?.responseUsage,
|
||||
modelOverride: persistedModelOverride ?? baseEntry?.modelOverride,
|
||||
providerOverride: persistedProviderOverride ?? baseEntry?.providerOverride,
|
||||
authProfileOverride: persistedAuthProfileOverride ?? baseEntry?.authProfileOverride,
|
||||
authProfileOverrideSource:
|
||||
persistedAuthProfileOverrideSource ?? baseEntry?.authProfileOverrideSource,
|
||||
authProfileOverrideCompactionCount:
|
||||
persistedAuthProfileOverrideCompactionCount ?? baseEntry?.authProfileOverrideCompactionCount,
|
||||
label: persistedLabel ?? baseEntry?.label,
|
||||
sendPolicy: baseEntry?.sendPolicy,
|
||||
queueMode: baseEntry?.queueMode,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user