diff --git a/src/plugin-handlers/config-handler.test.ts b/src/plugin-handlers/config-handler.test.ts new file mode 100644 index 0000000000..9724965fe3 --- /dev/null +++ b/src/plugin-handlers/config-handler.test.ts @@ -0,0 +1,104 @@ +import { describe, test, expect } from "bun:test" +import { resolveCategoryConfig } from "./config-handler" +import type { CategoryConfig } from "../config/schema" + +describe("Prometheus category config resolution", () => { + test("resolves ultrabrain category config", () => { + // #given + const categoryName = "ultrabrain" + + // #when + const config = resolveCategoryConfig(categoryName) + + // #then + expect(config).toBeDefined() + expect(config?.model).toBe("openai/gpt-5.2") + expect(config?.temperature).toBe(0.1) + }) + + test("resolves visual-engineering category config", () => { + // #given + const categoryName = "visual-engineering" + + // #when + const config = resolveCategoryConfig(categoryName) + + // #then + expect(config).toBeDefined() + expect(config?.model).toBe("google/gemini-3-pro-preview") + expect(config?.temperature).toBe(0.7) + }) + + test("user categories override default categories", () => { + // #given + const categoryName = "ultrabrain" + const userCategories: Record = { + ultrabrain: { + model: "google/antigravity-claude-opus-4-5-thinking", + temperature: 0.1, + }, + } + + // #when + const config = resolveCategoryConfig(categoryName, userCategories) + + // #then + expect(config).toBeDefined() + expect(config?.model).toBe("google/antigravity-claude-opus-4-5-thinking") + expect(config?.temperature).toBe(0.1) + }) + + test("returns undefined for unknown category", () => { + // #given + const categoryName = "nonexistent-category" + + // #when + const config = resolveCategoryConfig(categoryName) + + // #then + expect(config).toBeUndefined() + }) + + test("falls back to default when user category has no entry", () => { + // #given + const categoryName = "ultrabrain" + const userCategories: Record = { + "visual-engineering": { + model: "custom/visual-model", + }, + } + + // #when + const config = resolveCategoryConfig(categoryName, userCategories) + + // #then + expect(config).toBeDefined() + expect(config?.model).toBe("openai/gpt-5.2") + expect(config?.temperature).toBe(0.1) + }) + + test("preserves all category properties (temperature, top_p, tools, etc.)", () => { + // #given + const categoryName = "custom-category" + const userCategories: Record = { + "custom-category": { + model: "test/model", + temperature: 0.5, + top_p: 0.9, + maxTokens: 32000, + tools: { tool1: true, tool2: false }, + }, + } + + // #when + const config = resolveCategoryConfig(categoryName, userCategories) + + // #then + expect(config).toBeDefined() + expect(config?.model).toBe("test/model") + expect(config?.temperature).toBe(0.5) + expect(config?.top_p).toBe(0.9) + expect(config?.maxTokens).toBe(32000) + expect(config?.tools).toEqual({ tool1: true, tool2: false }) + }) +}) diff --git a/src/plugin-handlers/config-handler.ts b/src/plugin-handlers/config-handler.ts index c29efa7276..b16f8fb359 100644 --- a/src/plugin-handlers/config-handler.ts +++ b/src/plugin-handlers/config-handler.ts @@ -24,7 +24,9 @@ import type { OhMyOpenCodeConfig } from "../config"; import { log } from "../shared"; import { migrateAgentConfig } from "../shared/permission-compat"; import { PROMETHEUS_SYSTEM_PROMPT, PROMETHEUS_PERMISSION } from "../agents/prometheus-prompt"; +import { DEFAULT_CATEGORIES } from "../tools/sisyphus-task/constants"; import type { ModelCacheState } from "../plugin-state"; +import type { CategoryConfig } from "../config/schema"; export interface ConfigHandlerDeps { ctx: { directory: string }; @@ -32,6 +34,13 @@ export interface ConfigHandlerDeps { modelCacheState: ModelCacheState; } +export function resolveCategoryConfig( + categoryName: string, + userCategories?: Record +): CategoryConfig | undefined { + return userCategories?.[categoryName] ?? DEFAULT_CATEGORIES[categoryName]; +} + export function createConfigHandler(deps: ConfigHandlerDeps) { const { ctx, pluginConfig, modelCacheState } = deps; @@ -173,15 +182,50 @@ export function createConfigHandler(deps: ConfigHandlerDeps) { planConfigWithoutName as Record ); const prometheusOverride = - pluginConfig.agents?.["Prometheus (Planner)"]; + pluginConfig.agents?.["Prometheus (Planner)"] as + | (Record & { category?: string; model?: string }) + | undefined; const defaultModel = config.model as string | undefined; + + // Resolve full category config (model, temperature, top_p, tools, etc.) + // Apply all category properties when category is specified, but explicit + // overrides (model, temperature, etc.) will take precedence during merge + const categoryConfig = prometheusOverride?.category + ? resolveCategoryConfig( + prometheusOverride.category, + pluginConfig.categories + ) + : undefined; + const prometheusBase = { - model: defaultModel ?? "anthropic/claude-opus-4-5", + model: + prometheusOverride?.model ?? + categoryConfig?.model ?? + defaultModel ?? + "anthropic/claude-opus-4-5", mode: "primary" as const, prompt: PROMETHEUS_SYSTEM_PROMPT, permission: PROMETHEUS_PERMISSION, description: `${configAgent?.plan?.description ?? "Plan agent"} (Prometheus - OhMyOpenCode)`, color: (configAgent?.plan?.color as string) ?? "#FF6347", + // Apply category properties (temperature, top_p, tools, etc.) + ...(categoryConfig?.temperature !== undefined + ? { temperature: categoryConfig.temperature } + : {}), + ...(categoryConfig?.top_p !== undefined + ? { top_p: categoryConfig.top_p } + : {}), + ...(categoryConfig?.maxTokens !== undefined + ? { maxTokens: categoryConfig.maxTokens } + : {}), + ...(categoryConfig?.tools ? { tools: categoryConfig.tools } : {}), + ...(categoryConfig?.thinking ? { thinking: categoryConfig.thinking } : {}), + ...(categoryConfig?.reasoningEffort !== undefined + ? { reasoningEffort: categoryConfig.reasoningEffort } + : {}), + ...(categoryConfig?.textVerbosity !== undefined + ? { textVerbosity: categoryConfig.textVerbosity } + : {}), }; agentConfig["Prometheus (Planner)"] = prometheusOverride