mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-07 23:31:24 +00:00
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:
@@ -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();
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user