From dbf584af95c8e1aff22e34d558d736efcdd7e495 Mon Sep 17 00:00:00 2001 From: Rishi Vhavle Date: Sat, 7 Feb 2026 03:32:36 +0530 Subject: [PATCH] fix: respect user-configured agent models over system defaults When user explicitly configures an agent model in oh-my-opencode.json, that model should take priority over the active model in OpenCode's config (which may just be the system default, not a deliberate UI selection). This fixes the issue where user-configured models from plugin providers (e.g., google/antigravity-*) were being overridden by the fallback chain because config.model was being passed as uiSelectedModel regardless of whether the user had an explicit config. The fix: - Only pass uiSelectedModel when there's no explicit userModel config - If user has configured a model, let resolveModelPipeline use it directly Fixes #1573 --- src/agents/utils.test.ts | 118 +++++++++++++++++++++++++++++++++++++++ src/agents/utils.ts | 6 +- 2 files changed, 121 insertions(+), 3 deletions(-) diff --git a/src/agents/utils.test.ts b/src/agents/utils.test.ts index 8c0bda46..88883feb 100644 --- a/src/agents/utils.test.ts +++ b/src/agents/utils.test.ts @@ -79,6 +79,72 @@ describe("createBuiltinAgents with model overrides", () => { } }) + test("user config model takes priority over uiSelectedModel for sisyphus", async () => { + // #given + const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue( + new Set(["openai/gpt-5.2", "anthropic/claude-sonnet-4-5"]) + ) + const uiSelectedModel = "openai/gpt-5.2" + const overrides = { + sisyphus: { model: "google/antigravity-claude-opus-4-5-thinking" }, + } + + try { + // #when + const agents = await createBuiltinAgents( + [], + overrides, + undefined, + TEST_DEFAULT_MODEL, + undefined, + undefined, + [], + undefined, + undefined, + uiSelectedModel + ) + + // #then + expect(agents.sisyphus).toBeDefined() + expect(agents.sisyphus.model).toBe("google/antigravity-claude-opus-4-5-thinking") + } finally { + fetchSpy.mockRestore() + } + }) + + test("user config model takes priority over uiSelectedModel for atlas", async () => { + // #given + const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue( + new Set(["openai/gpt-5.2", "anthropic/claude-sonnet-4-5"]) + ) + const uiSelectedModel = "openai/gpt-5.2" + const overrides = { + atlas: { model: "google/antigravity-claude-opus-4-5-thinking" }, + } + + try { + // #when + const agents = await createBuiltinAgents( + [], + overrides, + undefined, + TEST_DEFAULT_MODEL, + undefined, + undefined, + [], + undefined, + undefined, + uiSelectedModel + ) + + // #then + expect(agents.atlas).toBeDefined() + expect(agents.atlas.model).toBe("google/antigravity-claude-opus-4-5-thinking") + } finally { + fetchSpy.mockRestore() + } + }) + test("Sisyphus is created on first run when no availableModels or cache exist", async () => { // #given const systemDefaultModel = "anthropic/claude-opus-4-6" @@ -422,6 +488,58 @@ describe("createBuiltinAgents with requiresAnyModel gating (sisyphus)", () => { cacheSpy.mockRestore() } }) + + test("sisyphus uses user-configured plugin model even when not in cache or fallback chain", async () => { + // #given - user configures a model from a plugin provider (like antigravity) + // that is NOT in the availableModels cache and NOT in the fallback chain + const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue( + new Set(["openai/gpt-5.2"]) + ) + const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue( + ["openai"] + ) + const overrides = { + sisyphus: { model: "google/antigravity-claude-opus-4-5-thinking" }, + } + + try { + // #when + const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL, undefined, undefined, [], {}) + + // #then + expect(agents.sisyphus).toBeDefined() + expect(agents.sisyphus.model).toBe("google/antigravity-claude-opus-4-5-thinking") + } finally { + fetchSpy.mockRestore() + cacheSpy.mockRestore() + } + }) + + test("sisyphus uses user-configured plugin model when availableModels is empty but cache exists", async () => { + // #given - connected providers cache exists but models cache is empty + // This reproduces the exact scenario where provider-models.json has models: {} + const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue( + new Set() + ) + const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue( + ["google", "openai", "opencode"] + ) + const overrides = { + sisyphus: { model: "google/antigravity-claude-opus-4-5-thinking" }, + } + + try { + // #when + const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL, undefined, undefined, [], {}) + + // #then + expect(agents.sisyphus).toBeDefined() + expect(agents.sisyphus.model).toBe("google/antigravity-claude-opus-4-5-thinking") + } finally { + fetchSpy.mockRestore() + cacheSpy.mockRestore() + } + }) }) describe("buildAgent with category and skills", () => { diff --git a/src/agents/utils.ts b/src/agents/utils.ts index b0b9c694..5aac0ebb 100644 --- a/src/agents/utils.ts +++ b/src/agents/utils.ts @@ -304,7 +304,7 @@ export async function createBuiltinAgents( const isPrimaryAgent = isFactory(source) && source.mode === "primary" const resolution = applyModelResolution({ - uiSelectedModel: isPrimaryAgent ? uiSelectedModel : undefined, + uiSelectedModel: (isPrimaryAgent && !override?.model) ? uiSelectedModel : undefined, userModel: override?.model, requirement, availableModels, @@ -356,7 +356,7 @@ export async function createBuiltinAgents( if (!disabledAgents.includes("sisyphus") && meetsSisyphusAnyModelRequirement) { let sisyphusResolution = applyModelResolution({ - uiSelectedModel, + uiSelectedModel: sisyphusOverride?.model ? undefined : uiSelectedModel, userModel: sisyphusOverride?.model, requirement: sisyphusRequirement, availableModels, @@ -454,7 +454,7 @@ export async function createBuiltinAgents( const atlasRequirement = AGENT_MODEL_REQUIREMENTS["atlas"] const atlasResolution = applyModelResolution({ - uiSelectedModel, + uiSelectedModel: orchestratorOverride?.model ? undefined : uiSelectedModel, userModel: orchestratorOverride?.model, requirement: atlasRequirement, availableModels,