From c06f38693e038aed8c579a9dacb7e9a01bcf3dd3 Mon Sep 17 00:00:00 2001 From: justsisyphus Date: Fri, 30 Jan 2026 14:05:47 +0900 Subject: [PATCH] refactor: revamp ultrabrain category with deep work mindset - Add variant: max to ultrabrain's gemini-3-pro fallback entry - Rename STRATEGIC_CATEGORY_PROMPT_APPEND to ULTRABRAIN_CATEGORY_PROMPT_APPEND - Keep original strategic advisor prompt content (no micromanagement instructions) - Update description: use only for genuinely hard tasks, give clear goals only - Update tests to match renamed constant --- src/shared/model-requirements.ts | 2 +- src/shared/model-resolver.test.ts | 97 +++++++++++++++++++++++++++ src/shared/model-resolver.ts | 35 +++++++++- src/tools/delegate-task/constants.ts | 8 +-- src/tools/delegate-task/tools.test.ts | 4 +- src/tools/delegate-task/tools.ts | 28 ++++---- 6 files changed, 152 insertions(+), 22 deletions(-) diff --git a/src/shared/model-requirements.ts b/src/shared/model-requirements.ts index a1bde2979..1607c5a4f 100644 --- a/src/shared/model-requirements.ts +++ b/src/shared/model-requirements.ts @@ -90,7 +90,7 @@ export const CATEGORY_MODEL_REQUIREMENTS: Record = { fallbackChain: [ { providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2-codex", variant: "xhigh" }, { providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-5", variant: "max" }, - { providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro" }, + { providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro", variant: "max" }, ], }, artistry: { diff --git a/src/shared/model-resolver.test.ts b/src/shared/model-resolver.test.ts index 9e1e665f2..d4b7fbd75 100644 --- a/src/shared/model-resolver.test.ts +++ b/src/shared/model-resolver.test.ts @@ -626,6 +626,103 @@ describe("resolveModelWithFallback", () => { }) }) + describe("categoryDefaultModel (fuzzy matching for category defaults)", () => { + test("applies fuzzy matching to categoryDefaultModel when userModel not provided", () => { + // #given - gemini-3-pro is the category default, but only gemini-3-pro-preview is available + const input: ExtendedModelResolutionInput = { + categoryDefaultModel: "google/gemini-3-pro", + fallbackChain: [ + { providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro" }, + ], + availableModels: new Set(["google/gemini-3-pro-preview", "anthropic/claude-opus-4-5"]), + systemDefaultModel: "anthropic/claude-sonnet-4-5", + } + + // #when + const result = resolveModelWithFallback(input) + + // #then - should fuzzy match gemini-3-pro → gemini-3-pro-preview + expect(result!.model).toBe("google/gemini-3-pro-preview") + expect(result!.source).toBe("category-default") + }) + + test("categoryDefaultModel uses exact match when available", () => { + // #given - exact match exists + const input: ExtendedModelResolutionInput = { + categoryDefaultModel: "google/gemini-3-pro", + fallbackChain: [ + { providers: ["google"], model: "gemini-3-pro" }, + ], + availableModels: new Set(["google/gemini-3-pro", "google/gemini-3-pro-preview"]), + systemDefaultModel: "anthropic/claude-sonnet-4-5", + } + + // #when + const result = resolveModelWithFallback(input) + + // #then - should use exact match + expect(result!.model).toBe("google/gemini-3-pro") + expect(result!.source).toBe("category-default") + }) + + test("categoryDefaultModel falls through to fallbackChain when no match in availableModels", () => { + // #given - categoryDefaultModel has no match, but fallbackChain does + const input: ExtendedModelResolutionInput = { + categoryDefaultModel: "google/gemini-3-pro", + fallbackChain: [ + { providers: ["anthropic"], model: "claude-opus-4-5" }, + ], + availableModels: new Set(["anthropic/claude-opus-4-5"]), + systemDefaultModel: "system/default", + } + + // #when + const result = resolveModelWithFallback(input) + + // #then - should fall through to fallbackChain + expect(result!.model).toBe("anthropic/claude-opus-4-5") + expect(result!.source).toBe("provider-fallback") + }) + + test("userModel takes priority over categoryDefaultModel", () => { + // #given - both userModel and categoryDefaultModel provided + const input: ExtendedModelResolutionInput = { + userModel: "anthropic/claude-opus-4-5", + categoryDefaultModel: "google/gemini-3-pro", + fallbackChain: [ + { providers: ["google"], model: "gemini-3-pro" }, + ], + availableModels: new Set(["google/gemini-3-pro-preview", "anthropic/claude-opus-4-5"]), + systemDefaultModel: "system/default", + } + + // #when + const result = resolveModelWithFallback(input) + + // #then - userModel wins + expect(result!.model).toBe("anthropic/claude-opus-4-5") + expect(result!.source).toBe("override") + }) + + test("categoryDefaultModel works when availableModels is empty but connected provider exists", () => { + // #given - no availableModels but connected provider cache exists + const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["google"]) + const input: ExtendedModelResolutionInput = { + categoryDefaultModel: "google/gemini-3-pro", + availableModels: new Set(), + systemDefaultModel: "anthropic/claude-sonnet-4-5", + } + + // #when + const result = resolveModelWithFallback(input) + + // #then - should use categoryDefaultModel since google is connected + expect(result!.model).toBe("google/gemini-3-pro") + expect(result!.source).toBe("category-default") + cacheSpy.mockRestore() + }) + }) + describe("Optional systemDefaultModel", () => { test("returns undefined when systemDefaultModel is undefined and no fallback found", () => { // #given diff --git a/src/shared/model-resolver.ts b/src/shared/model-resolver.ts index 41ca66511..792bf7fa1 100644 --- a/src/shared/model-resolver.ts +++ b/src/shared/model-resolver.ts @@ -11,6 +11,7 @@ export type ModelResolutionInput = { export type ModelSource = | "override" + | "category-default" | "provider-fallback" | "system-default" @@ -23,6 +24,7 @@ export type ModelResolutionResult = { export type ExtendedModelResolutionInput = { uiSelectedModel?: string userModel?: string + categoryDefaultModel?: string fallbackChain?: FallbackEntry[] availableModels: Set systemDefaultModel?: string @@ -44,7 +46,7 @@ export function resolveModel(input: ModelResolutionInput): string | undefined { export function resolveModelWithFallback( input: ExtendedModelResolutionInput, ): ModelResolutionResult | undefined { - const { uiSelectedModel, userModel, fallbackChain, availableModels, systemDefaultModel } = input + const { uiSelectedModel, userModel, categoryDefaultModel, fallbackChain, availableModels, systemDefaultModel } = input // Step 1: UI Selection (highest priority - respects user's model choice in OpenCode UI) const normalizedUiModel = normalizeModel(uiSelectedModel) @@ -53,13 +55,42 @@ export function resolveModelWithFallback( return { model: normalizedUiModel, source: "override" } } - // Step 2: Config Override (from oh-my-opencode.json) + // Step 2: Config Override (from oh-my-opencode.json user config) const normalizedUserModel = normalizeModel(userModel) if (normalizedUserModel) { log("Model resolved via config override", { model: normalizedUserModel }) return { model: normalizedUserModel, source: "override" } } + // Step 2.5: Category Default Model (from DEFAULT_CATEGORIES, with fuzzy matching) + const normalizedCategoryDefault = normalizeModel(categoryDefaultModel) + if (normalizedCategoryDefault) { + if (availableModels.size > 0) { + const parts = normalizedCategoryDefault.split("/") + const providerHint = parts.length >= 2 ? [parts[0]] : undefined + const match = fuzzyMatchModel(normalizedCategoryDefault, availableModels, providerHint) + if (match) { + log("Model resolved via category default (fuzzy matched)", { original: normalizedCategoryDefault, matched: match }) + return { model: match, source: "category-default" } + } + } else { + const connectedProviders = readConnectedProvidersCache() + if (connectedProviders === null) { + log("Model resolved via category default (no cache, first run)", { model: normalizedCategoryDefault }) + return { model: normalizedCategoryDefault, source: "category-default" } + } + const parts = normalizedCategoryDefault.split("/") + if (parts.length >= 2) { + const provider = parts[0] + if (connectedProviders.includes(provider)) { + log("Model resolved via category default (connected provider)", { model: normalizedCategoryDefault }) + return { model: normalizedCategoryDefault, source: "category-default" } + } + } + } + log("Category default model not available, falling through to fallback chain", { model: normalizedCategoryDefault }) + } + // Step 3: Provider fallback chain (exact match → fuzzy match → next provider) if (fallbackChain && fallbackChain.length > 0) { if (availableModels.size === 0) { diff --git a/src/tools/delegate-task/constants.ts b/src/tools/delegate-task/constants.ts index 4d9763ca0..be1e33aed 100644 --- a/src/tools/delegate-task/constants.ts +++ b/src/tools/delegate-task/constants.ts @@ -14,8 +14,8 @@ Design-first mindset: AVOID: Generic fonts, purple gradients on white, predictable layouts, cookie-cutter patterns. ` -export const STRATEGIC_CATEGORY_PROMPT_APPEND = ` -You are working on BUSINESS LOGIC / ARCHITECTURE tasks. +export const ULTRABRAIN_CATEGORY_PROMPT_APPEND = ` +You are working on DEEP LOGICAL REASONING / COMPLEX ARCHITECTURE tasks. Strategic advisor mindset: - Bias toward simplicity: least complex solution that fulfills requirements @@ -167,7 +167,7 @@ export const DEFAULT_CATEGORIES: Record = { export const CATEGORY_PROMPT_APPENDS: Record = { "visual-engineering": VISUAL_CATEGORY_PROMPT_APPEND, - ultrabrain: STRATEGIC_CATEGORY_PROMPT_APPEND, + ultrabrain: ULTRABRAIN_CATEGORY_PROMPT_APPEND, artistry: ARTISTRY_CATEGORY_PROMPT_APPEND, quick: QUICK_CATEGORY_PROMPT_APPEND, "unspecified-low": UNSPECIFIED_LOW_CATEGORY_PROMPT_APPEND, @@ -177,7 +177,7 @@ export const CATEGORY_PROMPT_APPENDS: Record = { export const CATEGORY_DESCRIPTIONS: Record = { "visual-engineering": "Frontend, UI/UX, design, styling, animation", - ultrabrain: "Deep logical reasoning, complex architecture decisions requiring extensive analysis", + ultrabrain: "Use ONLY for genuinely hard, logic-heavy tasks. Give clear goals only, not step-by-step instructions.", artistry: "Highly creative/artistic tasks, novel ideas", quick: "Trivial tasks - single file changes, typo fixes, simple modifications", "unspecified-low": "Tasks that don't fit other categories, low effort required", diff --git a/src/tools/delegate-task/tools.test.ts b/src/tools/delegate-task/tools.test.ts index c807954e7..8dbeb04e3 100644 --- a/src/tools/delegate-task/tools.test.ts +++ b/src/tools/delegate-task/tools.test.ts @@ -63,12 +63,12 @@ describe("sisyphus-task", () => { expect(promptAppend).toContain("Design-first") }) - test("ultrabrain category has strategic prompt", () => { + test("ultrabrain category has deep logical reasoning prompt", () => { // #given const promptAppend = CATEGORY_PROMPT_APPENDS["ultrabrain"] // #when / #then - expect(promptAppend).toContain("BUSINESS LOGIC") + expect(promptAppend).toContain("DEEP LOGICAL REASONING") expect(promptAppend).toContain("Strategic advisor") }) }) diff --git a/src/tools/delegate-task/tools.ts b/src/tools/delegate-task/tools.ts index 93ccc7041..e9f7730d2 100644 --- a/src/tools/delegate-task/tools.ts +++ b/src/tools/delegate-task/tools.ts @@ -541,7 +541,8 @@ To continue this session: session_id="${args.session_id}"` } } else { const resolution = resolveModelWithFallback({ - userModel: userCategories?.[args.category]?.model ?? resolved.model ?? sisyphusJuniorModel, + userModel: userCategories?.[args.category]?.model, + categoryDefaultModel: resolved.model ?? sisyphusJuniorModel, fallbackChain: requirement.fallbackChain, availableModels, systemDefaultModel, @@ -555,18 +556,19 @@ To continue this session: session_id="${args.session_id}"` return `Invalid model format "${actualModel}". Expected "provider/model" format (e.g., "anthropic/claude-sonnet-4-5").` } - let type: "user-defined" | "inherited" | "category-default" | "system-default" - switch (source) { - case "override": - type = "user-defined" - break - case "provider-fallback": - type = "category-default" - break - case "system-default": - type = "system-default" - break - } + let type: "user-defined" | "inherited" | "category-default" | "system-default" + switch (source) { + case "override": + type = "user-defined" + break + case "category-default": + case "provider-fallback": + type = "category-default" + break + case "system-default": + type = "system-default" + break + } modelInfo = { model: actualModel, type, source }