From 21713dcd86b4386f1c9a57e22733aab6fad00129 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Mon, 25 May 2026 11:06:37 +0900 Subject: [PATCH] fix(agents): preserve model overrides with team mode Filter host config agent aliases with the same protected builtin-name rules used for external agent sources so stale display-name entries cannot replace resolved user-configured models when team mode is enabled. Fixes #4429 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/agents/utils.test.ts | 98 +++++++++++++++++++ .../agent-config-handler.test.ts | 56 +++++++++++ src/plugin-handlers/agent-config-handler.ts | 57 ++++++----- 3 files changed, 184 insertions(+), 27 deletions(-) diff --git a/src/agents/utils.test.ts b/src/agents/utils.test.ts index 0df4ef602..efae22033 100644 --- a/src/agents/utils.test.ts +++ b/src/agents/utils.test.ts @@ -30,6 +30,104 @@ afterEach(() => { }) describe("createBuiltinAgents with model overrides", () => { + test("user config models take priority when team_mode is enabled", async () => { + // #given + const providerModelsSpy = spyOn(connectedProvidersCache, "readProviderModelsCache").mockReturnValue(null) + const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(new Set()) + const overrides = { + sisyphus: { model: "openai/gpt-5.5" }, + explore: { model: "minimax-cn-coding-plan/MiniMax-M2.5-highspeed" }, + atlas: { model: "google/antigravity-claude-opus-4-5-thinking" }, + hephaestus: { model: "github-copilot/gpt-5.5" }, + } + + try { + // #when + const agentsWithTeamMode = await createBuiltinAgents( + [], + overrides, + undefined, + TEST_DEFAULT_MODEL, + undefined, + undefined, + [], + undefined, + undefined, + undefined, + undefined, + false, + false, + true + ) + + // #then + expect(agentsWithTeamMode.sisyphus.model).toBe("openai/gpt-5.5") + expect(agentsWithTeamMode.explore.model).toBe("minimax-cn-coding-plan/MiniMax-M2.5-highspeed") + expect(agentsWithTeamMode.atlas.model).toBe("google/antigravity-claude-opus-4-5-thinking") + expect(agentsWithTeamMode.hephaestus.model).toBe("github-copilot/gpt-5.5") + } finally { + providerModelsSpy.mockRestore() + fetchSpy.mockRestore() + } + }) + + test("team_mode does not change resolved models for user overrides", async () => { + // #given + const providerModelsSpy = spyOn(connectedProvidersCache, "readProviderModelsCache").mockReturnValue(null) + const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(new Set()) + const overrides = { + sisyphus: { model: "openai/gpt-5.5" }, + explore: { model: "minimax-cn-coding-plan/MiniMax-M2.5-highspeed" }, + atlas: { model: "google/antigravity-claude-opus-4-5-thinking" }, + hephaestus: { model: "github-copilot/gpt-5.5" }, + } + + try { + // #when + const agentsWithoutTeamMode = await createBuiltinAgents( + [], + overrides, + undefined, + TEST_DEFAULT_MODEL, + undefined, + undefined, + [], + undefined, + undefined, + undefined, + undefined, + false, + false, + false + ) + const agentsWithTeamMode = await createBuiltinAgents( + [], + overrides, + undefined, + TEST_DEFAULT_MODEL, + undefined, + undefined, + [], + undefined, + undefined, + undefined, + undefined, + false, + false, + true + ) + + // #then + expect(agentsWithTeamMode.sisyphus.model).toBe(agentsWithoutTeamMode.sisyphus.model) + expect(agentsWithTeamMode.explore.model).toBe(agentsWithoutTeamMode.explore.model) + expect(agentsWithTeamMode.atlas.model).toBe(agentsWithoutTeamMode.atlas.model) + expect(agentsWithTeamMode.hephaestus.model).toBe(agentsWithoutTeamMode.hephaestus.model) + } finally { + providerModelsSpy.mockRestore() + fetchSpy.mockRestore() + } + }) + test("Sisyphus with default model has thinking config when all models available", async () => { // #given const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue( diff --git a/src/plugin-handlers/agent-config-handler.test.ts b/src/plugin-handlers/agent-config-handler.test.ts index 69d05ef1b..f2b2d7d08 100644 --- a/src/plugin-handlers/agent-config-handler.test.ts +++ b/src/plugin-handlers/agent-config-handler.test.ts @@ -310,6 +310,62 @@ describe("applyAgentConfig builtin override protection", () => { expect(result.SiSyPhUs).toBeUndefined() }) + test("filters host config agent display-name aliases before they override resolved builtin models", async () => { + // given + createBuiltinAgentsSpy.mockResolvedValue({ + sisyphus: { + name: "sisyphus", + prompt: "resolved sisyphus prompt", + mode: "primary", + model: "openai/gpt-5.5", + }, + explore: { + name: "explore", + prompt: "resolved explore prompt", + mode: "subagent", + model: "minimax-cn-coding-plan/MiniMax-M2.5-highspeed", + }, + atlas: builtinAtlasConfig, + }) + const config = createBaseConfig() + config.agent = { + [getAgentListDisplayName("sisyphus")]: { + name: getAgentListDisplayName("sisyphus"), + prompt: "stale sisyphus prompt", + mode: "primary", + model: "anthropic/claude-opus-4-7", + }, + [getAgentListDisplayName("explore")]: { + name: getAgentListDisplayName("explore"), + prompt: "stale explore prompt", + mode: "subagent", + model: "openai/gpt-5.4", + }, + } + const pluginConfig = { + ...createPluginConfig(), + team_mode: { enabled: true }, + agents: { + sisyphus: { model: "openai/gpt-5.5" }, + explore: { model: "minimax-cn-coding-plan/MiniMax-M2.5-highspeed" }, + }, + } as OhMyOpenCodeConfig + + // when + const result = await applyAgentConfig({ + config, + pluginConfig, + ctx: { directory: "/tmp" }, + pluginComponents: createPluginComponents(), + }) + + // then + expect((result[getAgentListDisplayName("sisyphus")] as AgentConfig).model).toBe("openai/gpt-5.5") + expect((result[getAgentListDisplayName("explore")] as AgentConfig).model).toBe( + "minimax-cn-coding-plan/MiniMax-M2.5-highspeed" + ) + }) + test("filters plugin agents whose key matches the builtin display-name alias", async () => { // given const pluginComponents = createPluginComponents() diff --git a/src/plugin-handlers/agent-config-handler.ts b/src/plugin-handlers/agent-config-handler.ts index cbfa33ba9..b090c507e 100644 --- a/src/plugin-handlers/agent-config-handler.ts +++ b/src/plugin-handlers/agent-config-handler.ts @@ -259,24 +259,6 @@ export async function applyAgentConfig(params: { agentConfig["OpenCode-Builder"] = override ? { ...base, ...override } : base; } - const filteredConfigAgents = configAgent - ? Object.fromEntries( - Object.entries(configAgent) - .filter(([key]) => { - if (key === "build") return false; - if (key === "plan" && shouldDemotePlan) return false; - if (key in builtinAgents) return false; - return true; - }) - .map(([key, value]) => { - if (!value) return [key, value]; - const migrated = migrateAgentConfig(value as Record); - if (!migrated.mode) migrated.mode = "subagent"; - return [key, migrated]; - }), - ) - : {}; - const migratedBuild = configAgent?.build ? migrateAgentConfig(configAgent.build as Record) : {}; @@ -292,6 +274,26 @@ export async function applyAgentConfig(params: { ...Object.keys(agentConfig), ...Object.keys(builtinAgents), ]); + const filteredConfigAgentSource = configAgent + ? filterProtectedAgentOverrides( + Object.fromEntries( + Object.entries(configAgent).filter(([key]) => { + if (key === "build") return false; + if (key === "plan" && shouldDemotePlan) return false; + return true; + }), + ), + protectedBuiltinAgentNames, + ) + : {}; + const filteredConfigAgents = Object.fromEntries( + Object.entries(filteredConfigAgentSource).map(([key, value]) => { + if (!value) return [key, value]; + const migrated = migrateAgentConfig(value as Record); + if (!migrated.mode) migrated.mode = "subagent"; + return [key, migrated]; + }), + ); const filteredUserAgents = filterProtectedAgentOverrides( userAgents, protectedBuiltinAgentNames, @@ -373,16 +375,17 @@ export async function applyAgentConfig(params: { protectedBuiltinAgentNames, ); - const defaultedConfigAgents = configAgent - ? Object.fromEntries( - Object.entries(configAgent).map(([key, value]) => { - if (!value) return [key, value]; - const migrated = migrateAgentConfig(value as Record); - if (!migrated.mode) migrated.mode = "subagent"; - return [key, migrated]; - }), - ) + const filteredConfigAgentSource = configAgent + ? filterProtectedAgentOverrides(configAgent, protectedBuiltinAgentNames) : {}; + const defaultedConfigAgents = Object.fromEntries( + Object.entries(filteredConfigAgentSource).map(([key, value]) => { + if (!value) return [key, value]; + const migrated = migrateAgentConfig(value as Record); + if (!migrated.mode) migrated.mode = "subagent"; + return [key, migrated]; + }), + ); params.config.agent = { ...builtinAgents,