fix(agents): resolve model aliases in sessions_spawn (#59681)

* fix(agents): resolve model aliases in sessions_spawn

normalizeModelSelection() only trims the input — it never resolves
aliases through the model alias index. When a user passes an alias
like 'opus' to sessions_spawn, the child session gets patched with
the raw string, which the gateway cannot match to any provider.

Add resolveModelThroughAliases() to check bare strings against the
configured alias map before returning from
resolveSubagentSpawnModelSelection().

Fixes #57532
Refs #50736

* refactor: address review feedback on alias resolution

- Accept pre-built ModelAliasIndex instead of rebuilding per call
- Narrow helper signature to (string, ModelAliasIndex) → string
- Remove unreachable ?? raw fallback

Co-Authored-By: greptile-apps[bot]

* fix(agents): resolve sessions_spawn model aliases

---------

Co-authored-by: HowdyDooToYou <HowdyDooToYou@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
This commit is contained in:
JK
2026-04-27 18:44:56 -04:00
committed by GitHub
parent c51e315f3a
commit 323030594e
3 changed files with 125 additions and 3 deletions

View File

@@ -110,6 +110,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Gateway/device tokens: stop echoing rotated bearer tokens from shared/admin `device.token.rotate` responses while preserving the same-device token handoff needed by token-only clients before reconnect. (#66773) Thanks @MoerAI.
- Agents/sessions_spawn: resolve configured bare model aliases for spawn model overrides using the target agent runtime default provider, carrying forward the alias-specific #69029 review fixes from #59681 without the unrelated active-session pruning path. Fixes #59681. Thanks @HowdyDooToYou.
- Control UI/Talk: keep Google Live browser sessions on the WebSocket transport instead of falling back to WebRTC, validate browser Google Live WebSocket endpoints, cap Gateway relay sessions per browser connection, and remove stale browser-native voice buttons that did not use the configured Talk/TTS provider. Thanks @BunsDev.
- Gateway/startup: reuse config snapshot plugin manifests for startup auto-enable before plugin bootstrap plans plugin loading. Thanks @shakkernerd.
- Agents/subagents: enforce `subagents.allowAgents` for explicit same-agent `sessions_spawn(agentId=...)` calls instead of auto-allowing requester self-targets. Fixes #72827. Thanks @oiGaDio.

View File

@@ -19,6 +19,7 @@ import {
resolveAllowedModelRef,
resolveConfiguredModelRef,
resolveSubagentConfiguredModelSelection,
resolveSubagentSpawnModelSelection,
resolveThinkingDefault,
resolveModelRefFromString,
} from "./model-selection.js";
@@ -1534,3 +1535,98 @@ describe("resolveSubagentConfiguredModelSelection", () => {
);
});
});
describe("resolveSubagentSpawnModelSelection", () => {
it("resolves a model alias override to its full provider/model ref", () => {
const cfg = {
agents: {
defaults: {
model: { primary: "anthropic/claude-sonnet-4-6" },
models: {
"anthropic/claude-opus-4-6": { alias: "opus" },
"openai/gpt-5.4": { alias: "gpt" },
},
},
},
} as OpenClawConfig;
expect(
resolveSubagentSpawnModelSelection({ cfg, agentId: "main", modelOverride: "opus" }),
).toBe("anthropic/claude-opus-4-6");
});
it("resolves bare configured aliases with the target agent runtime default provider", () => {
const cfg = {
agents: {
defaults: {
model: { primary: "openai/gpt-5.4" },
models: {
"claude-opus-4-6": { alias: "opus" },
},
},
list: [
{
id: "research",
model: "anthropic/claude-sonnet-4-6",
},
],
},
} as OpenClawConfig;
expect(
resolveSubagentSpawnModelSelection({
cfg,
agentId: "research",
modelOverride: "OPUS",
}),
).toBe("anthropic/claude-opus-4-6");
});
it("resolves alias in configured subagent model", () => {
const cfg = {
agents: {
defaults: {
model: { primary: "anthropic/claude-sonnet-4-6" },
models: {
"openai/gpt-5.4": { alias: "gpt" },
},
subagents: { model: "gpt" },
},
},
} as OpenClawConfig;
expect(resolveSubagentSpawnModelSelection({ cfg, agentId: "main" })).toBe("openai/gpt-5.4");
});
it("passes through already-qualified provider/model refs unchanged", () => {
const cfg = {
agents: {
defaults: {
model: { primary: "anthropic/claude-sonnet-4-6" },
},
},
} as OpenClawConfig;
expect(
resolveSubagentSpawnModelSelection({
cfg,
agentId: "main",
modelOverride: "openai/gpt-5.4",
}),
).toBe("openai/gpt-5.4");
});
it("falls back to runtime default when no override or config", () => {
const cfg = {
agents: {
defaults: {
model: { primary: "anthropic/claude-sonnet-4-6" },
},
},
} as OpenClawConfig;
expect(resolveSubagentSpawnModelSelection({ cfg, agentId: "main" })).toBe(
"anthropic/claude-sonnet-4-6",
);
});
});

View File

@@ -4,6 +4,7 @@ import {
toAgentModelListLike,
} from "../config/model-input.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
import {
resolveAgentConfig,
resolveAgentEffectiveModelPrimary,
@@ -248,6 +249,26 @@ export function resolveSubagentConfiguredModelSelection(params: {
);
}
/**
* Resolve a normalized model string through a pre-built alias index, returning
* a fully qualified `provider/model` string. If the value is already qualified
* or not a known alias, returns it unchanged.
*/
function resolveModelThroughAliases(value: string, aliasIndex: ModelAliasIndex): string {
// Already a provider/model ref — no alias resolution needed.
if (value.includes("/")) {
return value;
}
// Check if the value is a known alias; if so, resolve to provider/model.
// Unknown bare strings are returned as-is (don't guess the provider).
const aliasKey = normalizeLowercaseStringOrEmpty(value);
const aliasMatch = aliasIndex.byAlias.get(aliasKey);
if (aliasMatch) {
return `${aliasMatch.ref.provider}/${aliasMatch.ref.model}`;
}
return value;
}
export function resolveSubagentSpawnModelSelection(params: {
cfg: OpenClawConfig;
agentId: string;
@@ -257,15 +278,19 @@ export function resolveSubagentSpawnModelSelection(params: {
cfg: params.cfg,
agentId: params.agentId,
});
return (
const raw =
normalizeModelSelection(params.modelOverride) ??
resolveSubagentConfiguredModelSelection({
cfg: params.cfg,
agentId: params.agentId,
}) ??
normalizeModelSelection(resolveAgentModelPrimaryValue(params.cfg.agents?.defaults?.model)) ??
`${runtimeDefault.provider}/${runtimeDefault.model}`
);
`${runtimeDefault.provider}/${runtimeDefault.model}`;
const aliasIndex = buildModelAliasIndex({
cfg: params.cfg,
defaultProvider: runtimeDefault.provider,
});
return resolveModelThroughAliases(raw, aliasIndex);
}
export function buildAllowedModelSet(params: {