fix(cli): display correct model for sub-agents in sessions list (#18660)

Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: ba54c5a351
Co-authored-by: robbyczgw-cla <239660374+robbyczgw-cla@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
This commit is contained in:
Robby
2026-02-18 05:59:20 +01:00
committed by GitHub
parent a69e7682c1
commit 5c69e625f5
13 changed files with 485 additions and 67 deletions

View File

@@ -12,6 +12,7 @@ import {
parseGroupKey,
pruneLegacyStoreKeys,
resolveGatewaySessionStoreTarget,
resolveSessionModelRef,
resolveSessionStoreKey,
} from "./session-utils.js";
@@ -218,6 +219,47 @@ describe("gateway session utils", () => {
});
});
describe("resolveSessionModelRef", () => {
test("prefers runtime model/provider from session entry", () => {
const cfg = {
agents: {
defaults: {
model: { primary: "anthropic/claude-opus-4-6" },
},
},
} as OpenClawConfig;
const resolved = resolveSessionModelRef(cfg, {
sessionId: "s1",
updatedAt: Date.now(),
modelProvider: "openai-codex",
model: "gpt-5.3-codex",
modelOverride: "claude-opus-4-6",
providerOverride: "anthropic",
});
expect(resolved).toEqual({ provider: "openai-codex", model: "gpt-5.3-codex" });
});
test("falls back to override when runtime model is not recorded yet", () => {
const cfg = {
agents: {
defaults: {
model: { primary: "anthropic/claude-opus-4-6" },
},
},
} as OpenClawConfig;
const resolved = resolveSessionModelRef(cfg, {
sessionId: "s2",
updatedAt: Date.now(),
modelOverride: "openai-codex/gpt-5.3-codex",
});
expect(resolved).toEqual({ provider: "openai-codex", model: "gpt-5.3-codex" });
});
});
describe("deriveSessionTitle", () => {
test("returns undefined for undefined entry", () => {
expect(deriveSessionTitle(undefined)).toBeUndefined();

View File

@@ -4,6 +4,7 @@ import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent
import { lookupContextTokens } from "../agents/context.js";
import { DEFAULT_CONTEXT_TOKENS, DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js";
import {
parseModelRef,
resolveConfiguredModelRef,
resolveDefaultModelForAgent,
} from "../agents/model-selection.js";
@@ -643,7 +644,9 @@ export function getSessionDefaults(cfg: OpenClawConfig): GatewaySessionsDefaults
export function resolveSessionModelRef(
cfg: OpenClawConfig,
entry?: SessionEntry,
entry?:
| SessionEntry
| Pick<SessionEntry, "model" | "modelProvider" | "modelOverride" | "providerOverride">,
agentId?: string,
): { provider: string; model: string } {
const resolved = agentId
@@ -653,12 +656,41 @@ export function resolveSessionModelRef(
defaultProvider: DEFAULT_PROVIDER,
defaultModel: DEFAULT_MODEL,
});
// Prefer the last runtime model recorded on the session entry.
// This is the actual model used by the latest run and must win over defaults.
let provider = resolved.provider;
let model = resolved.model;
const runtimeModel = entry?.model?.trim();
const runtimeProvider = entry?.modelProvider?.trim();
if (runtimeModel) {
const parsedRuntime = parseModelRef(
runtimeModel,
runtimeProvider || provider || DEFAULT_PROVIDER,
);
if (parsedRuntime) {
provider = parsedRuntime.provider;
model = parsedRuntime.model;
} else {
provider = runtimeProvider || provider;
model = runtimeModel;
}
return { provider, model };
}
// Fall back to explicit per-session override (set at spawn/model-patch time),
// then finally to configured defaults.
const storedModelOverride = entry?.modelOverride?.trim();
if (storedModelOverride) {
provider = entry?.providerOverride?.trim() || provider;
model = storedModelOverride;
const overrideProvider = entry?.providerOverride?.trim() || provider || DEFAULT_PROVIDER;
const parsedOverride = parseModelRef(storedModelOverride, overrideProvider);
if (parsedOverride) {
provider = parsedOverride.provider;
model = parsedOverride.model;
} else {
provider = overrideProvider;
model = storedModelOverride;
}
}
return { provider, model };
}

View File

@@ -157,4 +157,125 @@ describe("gateway sessions patch", () => {
}
expect(res.error.message).toContain("spawnDepth is only supported");
});
test("allows target agent own model for subagent session even when missing from global allowlist", async () => {
const store: Record<string, SessionEntry> = {};
const cfg: OpenClawConfig = {
agents: {
defaults: {
model: { primary: "anthropic/claude-sonnet-4-6" },
models: {
"anthropic/claude-sonnet-4-6": { alias: "default" },
},
},
list: [
{
id: "kimi",
model: { primary: "synthetic/hf:moonshotai/Kimi-K2.5" },
},
],
},
} as OpenClawConfig;
const res = await applySessionsPatchToStore({
cfg,
store,
storeKey: "agent:kimi:subagent:child",
patch: {
key: "agent:kimi:subagent:child",
model: "synthetic/hf:moonshotai/Kimi-K2.5",
},
loadGatewayModelCatalog: async () => [
{ provider: "anthropic", id: "claude-sonnet-4-6", name: "sonnet" },
{ provider: "synthetic", id: "hf:moonshotai/Kimi-K2.5", name: "kimi" },
],
});
expect(res.ok).toBe(true);
if (!res.ok) {
return;
}
// Selected model matches the target agent default, so no override is stored.
expect(res.entry.providerOverride).toBeUndefined();
expect(res.entry.modelOverride).toBeUndefined();
});
test("allows target agent subagents.model for subagent session even when missing from global allowlist", async () => {
const store: Record<string, SessionEntry> = {};
const cfg: OpenClawConfig = {
agents: {
defaults: {
model: { primary: "anthropic/claude-sonnet-4-6" },
models: {
"anthropic/claude-sonnet-4-6": { alias: "default" },
},
},
list: [
{
id: "kimi",
model: { primary: "anthropic/claude-sonnet-4-6" },
subagents: { model: "synthetic/hf:moonshotai/Kimi-K2.5" },
},
],
},
} as OpenClawConfig;
const res = await applySessionsPatchToStore({
cfg,
store,
storeKey: "agent:kimi:subagent:child",
patch: {
key: "agent:kimi:subagent:child",
model: "synthetic/hf:moonshotai/Kimi-K2.5",
},
loadGatewayModelCatalog: async () => [
{ provider: "anthropic", id: "claude-sonnet-4-6", name: "sonnet" },
{ provider: "synthetic", id: "hf:moonshotai/Kimi-K2.5", name: "kimi" },
],
});
expect(res.ok).toBe(true);
if (!res.ok) {
return;
}
expect(res.entry.providerOverride).toBe("synthetic");
expect(res.entry.modelOverride).toBe("hf:moonshotai/Kimi-K2.5");
});
test("allows global defaults.subagents.model for subagent session even when missing from global allowlist", async () => {
const store: Record<string, SessionEntry> = {};
const cfg: OpenClawConfig = {
agents: {
defaults: {
model: { primary: "anthropic/claude-sonnet-4-6" },
subagents: { model: "synthetic/hf:moonshotai/Kimi-K2.5" },
models: {
"anthropic/claude-sonnet-4-6": { alias: "default" },
},
},
list: [{ id: "kimi", model: { primary: "anthropic/claude-sonnet-4-6" } }],
},
} as OpenClawConfig;
const res = await applySessionsPatchToStore({
cfg,
store,
storeKey: "agent:kimi:subagent:child",
patch: {
key: "agent:kimi:subagent:child",
model: "synthetic/hf:moonshotai/Kimi-K2.5",
},
loadGatewayModelCatalog: async () => [
{ provider: "anthropic", id: "claude-sonnet-4-6", name: "sonnet" },
{ provider: "synthetic", id: "hf:moonshotai/Kimi-K2.5", name: "kimi" },
],
});
expect(res.ok).toBe(true);
if (!res.ok) {
return;
}
expect(res.entry.providerOverride).toBe("synthetic");
expect(res.entry.modelOverride).toBe("hf:moonshotai/Kimi-K2.5");
});
});

View File

@@ -1,7 +1,11 @@
import { randomUUID } from "node:crypto";
import { resolveDefaultAgentId } from "../agents/agent-scope.js";
import type { ModelCatalogEntry } from "../agents/model-catalog.js";
import { resolveAllowedModelRef, resolveDefaultModelForAgent } from "../agents/model-selection.js";
import {
resolveAllowedModelRef,
resolveDefaultModelForAgent,
resolveSubagentConfiguredModelSelection,
} from "../agents/model-selection.js";
import { normalizeGroupActivation } from "../auto-reply/group-activation.js";
import {
formatThinkingLevels,
@@ -70,6 +74,9 @@ export async function applySessionsPatchToStore(params: {
const parsedAgent = parseAgentSessionKey(storeKey);
const sessionAgentId = normalizeAgentId(parsedAgent?.agentId ?? resolveDefaultAgentId(cfg));
const resolvedDefault = resolveDefaultModelForAgent({ cfg, agentId: sessionAgentId });
const subagentModelHint = isSubagentSessionKey(storeKey)
? resolveSubagentConfiguredModelSelection({ cfg, agentId: sessionAgentId })
: undefined;
const existing = store[storeKey];
const next: SessionEntry = existing
@@ -298,7 +305,7 @@ export async function applySessionsPatchToStore(params: {
catalog,
raw: trimmed,
defaultProvider: resolvedDefault.provider,
defaultModel: resolvedDefault.model,
defaultModel: subagentModelHint ?? resolvedDefault.model,
});
if ("error" in resolved) {
return invalid(resolved.error);