diff --git a/src/agents/utils.test.ts b/src/agents/utils.test.ts index 10783733..5b4462ba 100644 --- a/src/agents/utils.test.ts +++ b/src/agents/utils.test.ts @@ -32,30 +32,30 @@ describe("createBuiltinAgents with model overrides", () => { expect(agents.Sisyphus.thinking).toBeUndefined() }) - test("Sisyphus uses first fallbackChain entry when no availableModels provided", async () => { + test("Sisyphus uses system default when no availableModels provided", async () => { // #given - const systemDefaultModel = "openai/gpt-5.2" + const systemDefaultModel = "anthropic/claude-opus-4-5" // #when const agents = await createBuiltinAgents([], {}, undefined, systemDefaultModel) - // #then - Sisyphus first fallbackChain entry is anthropic/claude-opus-4-5 + // #then - falls back to system default when no availability match expect(agents.Sisyphus.model).toBe("anthropic/claude-opus-4-5") expect(agents.Sisyphus.thinking).toEqual({ type: "enabled", budgetTokens: 32000 }) expect(agents.Sisyphus.reasoningEffort).toBeUndefined() }) - test("Oracle uses first fallbackChain entry when no availableModels provided", async () => { - // #given - Oracle's first fallbackChain entry is openai/gpt-5.2 + test("Oracle uses system default when no availableModels provided", async () => { + // #given - no available models, falls back to system default // #when const agents = await createBuiltinAgents([], {}, undefined, TEST_DEFAULT_MODEL) - // #then - Oracle first fallbackChain entry is openai/gpt-5.2 - expect(agents.oracle.model).toBe("openai/gpt-5.2") - expect(agents.oracle.reasoningEffort).toBe("medium") - expect(agents.oracle.textVerbosity).toBe("high") - expect(agents.oracle.thinking).toBeUndefined() + // #then - falls back to system default (anthropic/claude-opus-4-5) + expect(agents.oracle.model).toBe("anthropic/claude-opus-4-5") + expect(agents.oracle.thinking).toEqual({ type: "enabled", budgetTokens: 32000 }) + expect(agents.oracle.reasoningEffort).toBeUndefined() + expect(agents.oracle.textVerbosity).toBeUndefined() }) test("Oracle with GPT model override has reasoningEffort, no thinking", async () => { diff --git a/src/index.ts b/src/index.ts index 7df6e557..fdd2d55f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -236,6 +236,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => { directory: ctx.directory, userCategories: pluginConfig.categories, gitMasterConfig: pluginConfig.git_master, + sisyphusJuniorModel: pluginConfig.agents?.["Sisyphus-Junior"]?.model, }); const disabledSkills = new Set(pluginConfig.disabled_skills ?? []); const systemMcpNames = getSystemMcpServerNames(); diff --git a/src/shared/model-resolver.test.ts b/src/shared/model-resolver.test.ts index 2f387a03..299210fa 100644 --- a/src/shared/model-resolver.test.ts +++ b/src/shared/model-resolver.test.ts @@ -316,8 +316,8 @@ describe("resolveModelWithFallback", () => { }) }) - describe("Step 3: First fallback entry (no availability match)", () => { - test("returns first fallbackChain entry when no availability match found", () => { + describe("Step 3: System default fallback (no availability match)", () => { + test("returns system default when no availability match found in fallback chain", () => { // #given const input: ExtendedModelResolutionInput = { fallbackChain: [ @@ -331,12 +331,12 @@ describe("resolveModelWithFallback", () => { const result = resolveModelWithFallback(input) // #then - expect(result.model).toBe("anthropic/nonexistent-model") - expect(result.source).toBe("provider-fallback") - expect(logSpy).toHaveBeenCalledWith("Model resolved via fallback chain first entry (no availability match)", { model: "anthropic/nonexistent-model", variant: undefined }) + expect(result.model).toBe("google/gemini-3-pro") + expect(result.source).toBe("system-default") + expect(logSpy).toHaveBeenCalledWith("No available model found in fallback chain, falling through to system default") }) - test("returns first fallbackChain entry when availableModels is empty", () => { + test("returns system default when availableModels is empty", () => { // #given const input: ExtendedModelResolutionInput = { fallbackChain: [ @@ -350,8 +350,8 @@ describe("resolveModelWithFallback", () => { const result = resolveModelWithFallback(input) // #then - expect(result.model).toBe("anthropic/claude-opus-4-5") - expect(result.source).toBe("provider-fallback") + expect(result.model).toBe("google/gemini-3-pro") + expect(result.source).toBe("system-default") }) test("returns system default when fallbackChain is not provided", () => { @@ -431,7 +431,7 @@ describe("resolveModelWithFallback", () => { expect(result.source).toBe("provider-fallback") }) - test("falls through to first fallbackChain entry when none match availability", () => { + test("falls through to system default when none match availability", () => { // #given const availableModels = new Set(["other/model"]) @@ -447,8 +447,8 @@ describe("resolveModelWithFallback", () => { }) // #then - expect(result.model).toBe("openai/gpt-5.2") - expect(result.source).toBe("provider-fallback") + expect(result.model).toBe("system/default") + expect(result.source).toBe("system-default") }) }) diff --git a/src/shared/model-resolver.ts b/src/shared/model-resolver.ts index 24432544..9881e883 100644 --- a/src/shared/model-resolver.ts +++ b/src/shared/model-resolver.ts @@ -63,15 +63,8 @@ export function resolveModelWithFallback( } } } - - // Step 3: Use first entry in fallbackChain as fallback (no availability match found) - // This ensures category/agent intent is honored even if availableModels is incomplete - const firstEntry = fallbackChain[0] - if (firstEntry.providers.length > 0) { - const fallbackModel = `${firstEntry.providers[0]}/${firstEntry.model}` - log("Model resolved via fallback chain first entry (no availability match)", { model: fallbackModel, variant: firstEntry.variant }) - return { model: fallbackModel, source: "provider-fallback", variant: firstEntry.variant } - } + // No match found in fallback chain - fall through to system default + log("No available model found in fallback chain, falling through to system default") } // Step 4: System default diff --git a/src/tools/delegate-task/tools.test.ts b/src/tools/delegate-task/tools.test.ts index 7c12d41d..35d71228 100644 --- a/src/tools/delegate-task/tools.test.ts +++ b/src/tools/delegate-task/tools.test.ts @@ -360,6 +360,7 @@ describe("sisyphus-task", () => { const mockClient = { app: { agents: async () => ({ data: [] }) }, config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, + model: { list: async () => [{ id: "anthropic/claude-opus-4-5" }] }, session: { create: async () => ({ data: { id: "test-session" } }), prompt: async () => ({ data: {} }), @@ -410,6 +411,7 @@ describe("sisyphus-task", () => { const mockClient = { app: { agents: async () => ({ data: [] }) }, config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, + model: { list: async () => [{ id: "anthropic/claude-opus-4-5" }] }, session: { get: async () => ({ data: { directory: "/project" } }), create: async () => ({ data: { id: "ses_sync_default_variant" } }), @@ -958,6 +960,7 @@ describe("sisyphus-task", () => { const mockClient = { app: { agents: async () => ({ data: [] }) }, config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, + model: { list: async () => [{ id: "google/gemini-3-pro-preview" }] }, session: { get: async () => ({ data: { directory: "/project" } }), create: async () => ({ data: { id: "ses_unstable_gemini" } }), @@ -1141,6 +1144,7 @@ describe("sisyphus-task", () => { const mockClient = { app: { agents: async () => ({ data: [] }) }, config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, + model: { list: async () => [{ id: "google/gemini-3-pro-preview" }] }, session: { get: async () => ({ data: { directory: "/project" } }), create: async () => ({ data: { id: "ses_artistry_gemini" } }), @@ -1205,6 +1209,7 @@ describe("sisyphus-task", () => { const mockClient = { app: { agents: async () => ({ data: [] }) }, config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, + model: { list: async () => [{ id: "google/gemini-3-flash-preview" }] }, session: { get: async () => ({ data: { directory: "/project" } }), create: async () => ({ data: { id: "ses_writing_gemini" } }), diff --git a/src/tools/delegate-task/tools.ts b/src/tools/delegate-task/tools.ts index 043667bd..9c3bed2c 100644 --- a/src/tools/delegate-task/tools.ts +++ b/src/tools/delegate-task/tools.ts @@ -156,6 +156,7 @@ export interface DelegateTaskToolOptions { directory: string userCategories?: CategoriesConfig gitMasterConfig?: GitMasterConfig + sisyphusJuniorModel?: string } export interface BuildSystemContentInput { @@ -178,7 +179,7 @@ export function buildSystemContent(input: BuildSystemContentInput): string | und } export function createDelegateTask(options: DelegateTaskToolOptions): ToolDefinition { - const { manager, client, directory, userCategories, gitMasterConfig } = options + const { manager, client, directory, userCategories, gitMasterConfig, sisyphusJuniorModel } = options const allCategories = { ...DEFAULT_CATEGORIES, ...userCategories } const categoryNames = Object.keys(allCategories) @@ -513,7 +514,7 @@ To resume this session: resume="${args.resume}"` modelInfo = { model: actualModel, type: "system-default", source: "system-default" } } else { const { model: resolvedModel, source, variant: resolvedVariant } = resolveModelWithFallback({ - userModel: userCategories?.[args.category]?.model, + userModel: userCategories?.[args.category]?.model ?? sisyphusJuniorModel, fallbackChain: requirement.fallbackChain, availableModels, systemDefaultModel,