diff --git a/src/agents/sisyphus-junior.ts b/src/agents/sisyphus-junior.ts index 671983a10..690b3eeba 100644 --- a/src/agents/sisyphus-junior.ts +++ b/src/agents/sisyphus-junior.ts @@ -84,13 +84,14 @@ export const SISYPHUS_JUNIOR_DEFAULTS = { } as const export function createSisyphusJuniorAgentWithOverrides( - override: AgentOverrideConfig | undefined + override: AgentOverrideConfig | undefined, + systemDefaultModel?: string ): AgentConfig { if (override?.disable) { override = undefined } - const model = override?.model ?? SISYPHUS_JUNIOR_DEFAULTS.model + const model = override?.model ?? systemDefaultModel ?? SISYPHUS_JUNIOR_DEFAULTS.model const temperature = override?.temperature ?? SISYPHUS_JUNIOR_DEFAULTS.temperature const promptAppend = override?.prompt_append diff --git a/src/agents/utils.ts b/src/agents/utils.ts index 808a6ef36..d831caa87 100644 --- a/src/agents/utils.ts +++ b/src/agents/utils.ts @@ -192,7 +192,7 @@ export function createBuiltinAgents( if (!disabledAgents.includes("orchestrator-sisyphus")) { const orchestratorOverride = agentOverrides["orchestrator-sisyphus"] - const orchestratorModel = orchestratorOverride?.model + const orchestratorModel = orchestratorOverride?.model ?? systemDefaultModel let orchestratorConfig = createOrchestratorSisyphusAgent({ model: orchestratorModel, availableAgents, diff --git a/src/features/task-toast-manager/index.ts b/src/features/task-toast-manager/index.ts index f779eee8c..26d91af03 100644 --- a/src/features/task-toast-manager/index.ts +++ b/src/features/task-toast-manager/index.ts @@ -1,2 +1,2 @@ export { TaskToastManager, getTaskToastManager, initTaskToastManager } from "./manager" -export type { TrackedTask, TaskStatus, TaskToastOptions } from "./types" +export type { TrackedTask, TaskStatus, TaskToastOptions, ModelFallbackInfo } from "./types" diff --git a/src/features/task-toast-manager/manager.test.ts b/src/features/task-toast-manager/manager.test.ts index 1e813ba85..9558fe8d2 100644 --- a/src/features/task-toast-manager/manager.test.ts +++ b/src/features/task-toast-manager/manager.test.ts @@ -142,4 +142,87 @@ describe("TaskToastManager", () => { expect(call.body.message).toContain("Running (1):") }) }) + + describe("model fallback info in toast message", () => { + test("should display warning when model falls back to default", () => { + // #given - a task with model fallback to default + const task = { + id: "task_1", + description: "Task with default model", + agent: "Sisyphus-Junior", + isBackground: false, + modelInfo: { model: "anthropic/claude-sonnet-4-5", type: "default" as const }, + } + + // #when - addTask is called + toastManager.addTask(task) + + // #then - toast should show warning with model info + expect(mockClient.tui.showToast).toHaveBeenCalled() + const call = mockClient.tui.showToast.mock.calls[0][0] + expect(call.body.message).toContain("⚠️") + expect(call.body.message).toContain("anthropic/claude-sonnet-4-5") + expect(call.body.message).toContain("(default)") + }) + + test("should display warning when model is inherited from parent", () => { + // #given - a task with inherited model + const task = { + id: "task_2", + description: "Task with inherited model", + agent: "Sisyphus-Junior", + isBackground: false, + modelInfo: { model: "cliproxy/claude-opus-4-5", type: "inherited" as const }, + } + + // #when - addTask is called + toastManager.addTask(task) + + // #then - toast should show warning with inherited model + expect(mockClient.tui.showToast).toHaveBeenCalled() + const call = mockClient.tui.showToast.mock.calls[0][0] + expect(call.body.message).toContain("⚠️") + expect(call.body.message).toContain("cliproxy/claude-opus-4-5") + expect(call.body.message).toContain("(inherited)") + }) + + test("should not display model info when user-defined", () => { + // #given - a task with user-defined model + const task = { + id: "task_3", + description: "Task with user model", + agent: "Sisyphus-Junior", + isBackground: false, + modelInfo: { model: "my-provider/my-model", type: "user-defined" as const }, + } + + // #when - addTask is called + toastManager.addTask(task) + + // #then - toast should NOT show model warning + expect(mockClient.tui.showToast).toHaveBeenCalled() + const call = mockClient.tui.showToast.mock.calls[0][0] + expect(call.body.message).not.toContain("⚠️ Model:") + expect(call.body.message).not.toContain("(inherited)") + expect(call.body.message).not.toContain("(default)") + }) + + test("should not display model info when not provided", () => { + // #given - a task without model info + const task = { + id: "task_4", + description: "Task without model info", + agent: "explore", + isBackground: true, + } + + // #when - addTask is called + toastManager.addTask(task) + + // #then - toast should NOT show model warning + expect(mockClient.tui.showToast).toHaveBeenCalled() + const call = mockClient.tui.showToast.mock.calls[0][0] + expect(call.body.message).not.toContain("⚠️ Model:") + }) + }) }) diff --git a/src/features/task-toast-manager/manager.ts b/src/features/task-toast-manager/manager.ts index 66a03b2a2..20086c6ab 100644 --- a/src/features/task-toast-manager/manager.ts +++ b/src/features/task-toast-manager/manager.ts @@ -1,5 +1,5 @@ import type { PluginInput } from "@opencode-ai/plugin" -import type { TrackedTask, TaskStatus } from "./types" +import type { TrackedTask, TaskStatus, ModelFallbackInfo } from "./types" import type { ConcurrencyManager } from "../background-agent/concurrency" type OpencodeClient = PluginInput["client"] @@ -25,6 +25,7 @@ export class TaskToastManager { isBackground: boolean status?: TaskStatus skills?: string[] + modelInfo?: ModelFallbackInfo }): void { const trackedTask: TrackedTask = { id: task.id, @@ -34,6 +35,7 @@ export class TaskToastManager { startedAt: new Date(), isBackground: task.isBackground, skills: task.skills, + modelInfo: task.modelInfo, } this.tasks.set(task.id, trackedTask) @@ -105,6 +107,14 @@ export class TaskToastManager { const lines: string[] = [] + // Show model fallback warning for the new task if applicable + if (newTask.modelInfo && newTask.modelInfo.type !== "user-defined") { + const icon = "⚠️" + const suffix = newTask.modelInfo.type === "inherited" ? " (inherited)" : " (default)" + lines.push(`${icon} Model: ${newTask.modelInfo.model}${suffix}`) + lines.push("") + } + if (running.length > 0) { lines.push(`Running (${running.length}):${concurrencyInfo}`) for (const task of running) { diff --git a/src/features/task-toast-manager/types.ts b/src/features/task-toast-manager/types.ts index de4aca0a0..5132e1479 100644 --- a/src/features/task-toast-manager/types.ts +++ b/src/features/task-toast-manager/types.ts @@ -1,5 +1,10 @@ export type TaskStatus = "running" | "queued" | "completed" | "error" +export interface ModelFallbackInfo { + model: string + type: "user-defined" | "inherited" | "default" +} + export interface TrackedTask { id: string description: string @@ -8,6 +13,7 @@ export interface TrackedTask { startedAt: Date isBackground: boolean skills?: string[] + modelInfo?: ModelFallbackInfo } export interface TaskToastOptions { diff --git a/src/plugin-handlers/config-handler.ts b/src/plugin-handlers/config-handler.ts index 96ff156f6..55c4f24e5 100644 --- a/src/plugin-handlers/config-handler.ts +++ b/src/plugin-handlers/config-handler.ts @@ -154,7 +154,8 @@ export function createConfigHandler(deps: ConfigHandlerDeps) { }; agentConfig["Sisyphus-Junior"] = createSisyphusJuniorAgentWithOverrides( - pluginConfig.agents?.["Sisyphus-Junior"] + pluginConfig.agents?.["Sisyphus-Junior"], + config.model as string | undefined ); if (builderEnabled) { diff --git a/src/tools/sisyphus-task/tools.ts b/src/tools/sisyphus-task/tools.ts index d4b720793..127560e9e 100644 --- a/src/tools/sisyphus-task/tools.ts +++ b/src/tools/sisyphus-task/tools.ts @@ -9,6 +9,7 @@ import { findNearestMessageWithFields, findFirstMessageWithAgent, MESSAGE_STORAG import { resolveMultipleSkills } from "../../features/opencode-skill-loader/skill-content" import { createBuiltinSkills } from "../../features/builtin-skills/skills" import { getTaskToastManager } from "../../features/task-toast-manager" +import type { ModelFallbackInfo } from "../../features/task-toast-manager/types" import { subagentSessions, getSessionAgent } from "../../features/claude-code-session-state" import { log } from "../../shared/logger" @@ -60,7 +61,8 @@ type ToolContextWithMetadata = { function resolveCategoryConfig( categoryName: string, - userCategories?: CategoriesConfig + userCategories?: CategoriesConfig, + parentModelString?: string ): { config: CategoryConfig; promptAppend: string } | null { const defaultConfig = DEFAULT_CATEGORIES[categoryName] const userConfig = userCategories?.[categoryName] @@ -70,10 +72,12 @@ function resolveCategoryConfig( return null } + // Model priority: user override > parent model (inherit) > category default > hardcoded fallback + // Parent model takes precedence over category default so custom providers work out-of-box const config: CategoryConfig = { ...defaultConfig, ...userConfig, - model: userConfig?.model ?? defaultConfig?.model ?? "anthropic/claude-sonnet-4-5", + model: userConfig?.model ?? parentModelString ?? defaultConfig?.model ?? "anthropic/claude-sonnet-4-5", } let promptAppend = defaultPromptAppend @@ -329,12 +333,27 @@ ${textContent || "(No text output)"}` let categoryModel: { providerID: string; modelID: string; variant?: string } | undefined let categoryPromptAppend: string | undefined + const parentModelString = parentModel + ? `${parentModel.providerID}/${parentModel.modelID}` + : undefined + + let modelInfo: ModelFallbackInfo | undefined + if (args.category) { - const resolved = resolveCategoryConfig(args.category, userCategories) + const resolved = resolveCategoryConfig(args.category, userCategories, parentModelString) if (!resolved) { return `❌ Unknown category: "${args.category}". Available: ${Object.keys({ ...DEFAULT_CATEGORIES, ...userCategories }).join(", ")}` } + const userHasDefinedCategory = userCategories?.[args.category]?.model !== undefined + if (userHasDefinedCategory) { + modelInfo = { model: resolved.config.model, type: "user-defined" } + } else if (parentModelString) { + modelInfo = { model: parentModelString, type: "inherited" } + } else if (DEFAULT_CATEGORIES[args.category]?.model) { + modelInfo = { model: DEFAULT_CATEGORIES[args.category].model, type: "default" } + } + agentToUse = SISYPHUS_JUNIOR_AGENT const parsedModel = parseModelString(resolved.config.model) categoryModel = parsedModel @@ -448,6 +467,7 @@ System notifies on completion. Use \`background_output\` with task_id="${task.id agent: agentToUse, isBackground: false, skills: args.skills, + modelInfo, }) }