mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-07 05:31:38 +00:00
fix(agents): honor heartbeat.model override instead of session model (#14181)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: f19b789057
Co-authored-by: 0xRaini <190923101+0xRaini@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
This commit is contained in:
@@ -106,6 +106,7 @@ export async function resolveReplyDirectives(params: {
|
||||
aliasIndex: ModelAliasIndex;
|
||||
provider: string;
|
||||
model: string;
|
||||
hasResolvedHeartbeatModelOverride: boolean;
|
||||
typing: TypingController;
|
||||
opts?: GetReplyOptions;
|
||||
skillFilter?: string[];
|
||||
@@ -131,6 +132,7 @@ export async function resolveReplyDirectives(params: {
|
||||
defaultModel,
|
||||
provider: initialProvider,
|
||||
model: initialModel,
|
||||
hasResolvedHeartbeatModelOverride,
|
||||
typing,
|
||||
opts,
|
||||
skillFilter,
|
||||
@@ -391,6 +393,7 @@ export async function resolveReplyDirectives(params: {
|
||||
provider,
|
||||
model,
|
||||
hasModelDirective: directives.hasModelDirective,
|
||||
hasResolvedHeartbeatModelOverride,
|
||||
});
|
||||
provider = modelState.provider;
|
||||
model = modelState.model;
|
||||
|
||||
@@ -78,6 +78,7 @@ export async function getReplyFromConfig(
|
||||
});
|
||||
let provider = defaultProvider;
|
||||
let model = defaultModel;
|
||||
let hasResolvedHeartbeatModelOverride = false;
|
||||
if (opts?.isHeartbeat) {
|
||||
const heartbeatRaw = agentCfg?.heartbeat?.model?.trim() ?? "";
|
||||
const heartbeatRef = heartbeatRaw
|
||||
@@ -90,6 +91,7 @@ export async function getReplyFromConfig(
|
||||
if (heartbeatRef) {
|
||||
provider = heartbeatRef.ref.provider;
|
||||
model = heartbeatRef.ref.model;
|
||||
hasResolvedHeartbeatModelOverride = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,6 +198,7 @@ export async function getReplyFromConfig(
|
||||
aliasIndex,
|
||||
provider,
|
||||
model,
|
||||
hasResolvedHeartbeatModelOverride,
|
||||
typing,
|
||||
opts: resolvedOpts,
|
||||
skillFilter: mergedSkillFilter,
|
||||
|
||||
@@ -153,4 +153,62 @@ describe("createModelSelectionState parent inheritance", () => {
|
||||
expect(state.provider).toBe(defaultProvider);
|
||||
expect(state.model).toBe(defaultModel);
|
||||
});
|
||||
|
||||
it("applies stored override when heartbeat override was not resolved", async () => {
|
||||
const cfg = {} as OpenClawConfig;
|
||||
const sessionKey = "agent:main:discord:channel:c1";
|
||||
const sessionEntry = makeEntry({
|
||||
providerOverride: "openai",
|
||||
modelOverride: "gpt-4o",
|
||||
});
|
||||
const sessionStore = {
|
||||
[sessionKey]: sessionEntry,
|
||||
};
|
||||
|
||||
const state = await createModelSelectionState({
|
||||
cfg,
|
||||
agentCfg: cfg.agents?.defaults,
|
||||
sessionEntry,
|
||||
sessionStore,
|
||||
sessionKey,
|
||||
defaultProvider,
|
||||
defaultModel,
|
||||
provider: "anthropic",
|
||||
model: "claude-opus-4-5",
|
||||
hasModelDirective: false,
|
||||
hasResolvedHeartbeatModelOverride: false,
|
||||
});
|
||||
|
||||
expect(state.provider).toBe("openai");
|
||||
expect(state.model).toBe("gpt-4o");
|
||||
});
|
||||
|
||||
it("skips stored override when heartbeat override was resolved", async () => {
|
||||
const cfg = {} as OpenClawConfig;
|
||||
const sessionKey = "agent:main:discord:channel:c1";
|
||||
const sessionEntry = makeEntry({
|
||||
providerOverride: "openai",
|
||||
modelOverride: "gpt-4o",
|
||||
});
|
||||
const sessionStore = {
|
||||
[sessionKey]: sessionEntry,
|
||||
};
|
||||
|
||||
const state = await createModelSelectionState({
|
||||
cfg,
|
||||
agentCfg: cfg.agents?.defaults,
|
||||
sessionEntry,
|
||||
sessionStore,
|
||||
sessionKey,
|
||||
defaultProvider,
|
||||
defaultModel,
|
||||
provider: "anthropic",
|
||||
model: "claude-opus-4-5",
|
||||
hasModelDirective: false,
|
||||
hasResolvedHeartbeatModelOverride: true,
|
||||
});
|
||||
|
||||
expect(state.provider).toBe("anthropic");
|
||||
expect(state.model).toBe("claude-opus-4-5");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -271,6 +271,9 @@ export async function createModelSelectionState(params: {
|
||||
provider: string;
|
||||
model: string;
|
||||
hasModelDirective: boolean;
|
||||
/** True when heartbeat.model was explicitly resolved for this run.
|
||||
* In that case, skip session-stored overrides so the heartbeat selection wins. */
|
||||
hasResolvedHeartbeatModelOverride?: boolean;
|
||||
}): Promise<ModelSelectionState> {
|
||||
const {
|
||||
cfg,
|
||||
@@ -343,7 +346,11 @@ export async function createModelSelectionState(params: {
|
||||
sessionKey,
|
||||
parentSessionKey,
|
||||
});
|
||||
if (storedOverride?.model) {
|
||||
// Skip stored session model override only when an explicit heartbeat.model
|
||||
// was resolved. Heartbeat runs without heartbeat.model should still inherit
|
||||
// the regular session/parent model override behavior.
|
||||
const skipStoredOverride = params.hasResolvedHeartbeatModelOverride === true;
|
||||
if (storedOverride?.model && !skipStoredOverride) {
|
||||
const candidateProvider = storedOverride.provider || defaultProvider;
|
||||
const key = modelKey(candidateProvider, storedOverride.model);
|
||||
if (allowedModelKeys.size === 0 || allowedModelKeys.has(key)) {
|
||||
|
||||
Reference in New Issue
Block a user