fix: remove hardcoded model defaults from categories and agents

BREAKING CHANGE: Model resolution overhauled

- Created centralized model-resolver.ts with priority chain:
  userModel → inheritedModel → systemDefaultModel
- Removed model field from all 7 DEFAULT_CATEGORIES entries
- Removed DEFAULT_MODEL constants from 10 agents
- Removed singleton agent exports (use factories instead)
- Made CategoryConfigSchema.model optional
- CLI no longer generates model overrides
- Empty strings treated as unset (uses fallback)

Users must now:
1. Use factory functions (createOracleAgent, etc.) instead of singletons
2. Provide model explicitly or use systemDefaultModel
3. Configure category models explicitly if needed

Fixes model fallback bug where hardcoded defaults overrode
user's OpenCode configured model.
This commit is contained in:
Kenny
2026-01-17 12:51:03 -05:00
parent be1af6d980
commit 76508cfc7d
31 changed files with 459 additions and 496 deletions
+1 -6
View File
@@ -2,8 +2,6 @@ import type { AgentConfig } from "@opencode-ai/sdk"
import type { AgentPromptMetadata } from "./types"
import { createAgentToolRestrictions } from "../shared/permission-compat"
const DEFAULT_MODEL = "google/gemini-3-flash-preview"
export const DOCUMENT_WRITER_PROMPT_METADATA: AgentPromptMetadata = {
category: "specialist",
cost: "CHEAP",
@@ -13,9 +11,7 @@ export const DOCUMENT_WRITER_PROMPT_METADATA: AgentPromptMetadata = {
],
}
export function createDocumentWriterAgent(
model: string = DEFAULT_MODEL
): AgentConfig {
export function createDocumentWriterAgent(model: string): AgentConfig {
const restrictions = createAgentToolRestrictions([])
return {
@@ -221,4 +217,3 @@ You are a technical writer who creates documentation that developers actually wa
}
}
export const documentWriterAgent = createDocumentWriterAgent()
+1 -4
View File
@@ -2,8 +2,6 @@ import type { AgentConfig } from "@opencode-ai/sdk"
import type { AgentPromptMetadata } from "./types"
import { createAgentToolRestrictions } from "../shared/permission-compat"
const DEFAULT_MODEL = "opencode/grok-code"
export const EXPLORE_PROMPT_METADATA: AgentPromptMetadata = {
category: "exploration",
cost: "FREE",
@@ -24,7 +22,7 @@ export const EXPLORE_PROMPT_METADATA: AgentPromptMetadata = {
],
}
export function createExploreAgent(model: string = DEFAULT_MODEL): AgentConfig {
export function createExploreAgent(model: string): AgentConfig {
const restrictions = createAgentToolRestrictions([
"write",
"edit",
@@ -122,4 +120,3 @@ Flood with parallel calls. Cross-validate findings across multiple tools.`,
}
}
export const exploreAgent = createExploreAgent()
+1 -6
View File
@@ -2,8 +2,6 @@ import type { AgentConfig } from "@opencode-ai/sdk"
import type { AgentPromptMetadata } from "./types"
import { createAgentToolRestrictions } from "../shared/permission-compat"
const DEFAULT_MODEL = "google/gemini-3-pro-preview"
export const FRONTEND_PROMPT_METADATA: AgentPromptMetadata = {
category: "specialist",
cost: "CHEAP",
@@ -19,9 +17,7 @@ export const FRONTEND_PROMPT_METADATA: AgentPromptMetadata = {
],
}
export function createFrontendUiUxEngineerAgent(
model: string = DEFAULT_MODEL
): AgentConfig {
export function createFrontendUiUxEngineerAgent(model: string): AgentConfig {
const restrictions = createAgentToolRestrictions([])
return {
@@ -106,4 +102,3 @@ Interpret creatively and make unexpected choices that feel genuinely designed fo
}
}
export const frontendUiUxEngineerAgent = createFrontendUiUxEngineerAgent()
+10 -25
View File
@@ -1,28 +1,13 @@
import type { AgentConfig } from "@opencode-ai/sdk"
import { sisyphusAgent } from "./sisyphus"
import { oracleAgent } from "./oracle"
import { librarianAgent } from "./librarian"
import { exploreAgent } from "./explore"
import { frontendUiUxEngineerAgent } from "./frontend-ui-ux-engineer"
import { documentWriterAgent } from "./document-writer"
import { multimodalLookerAgent } from "./multimodal-looker"
import { metisAgent } from "./metis"
import { orchestratorSisyphusAgent } from "./orchestrator-sisyphus"
import { momusAgent } from "./momus"
export const builtinAgents: Record<string, AgentConfig> = {
Sisyphus: sisyphusAgent,
oracle: oracleAgent,
librarian: librarianAgent,
explore: exploreAgent,
"frontend-ui-ux-engineer": frontendUiUxEngineerAgent,
"document-writer": documentWriterAgent,
"multimodal-looker": multimodalLookerAgent,
"Metis (Plan Consultant)": metisAgent,
"Momus (Plan Reviewer)": momusAgent,
"orchestrator-sisyphus": orchestratorSisyphusAgent,
}
export * from "./types"
export { createBuiltinAgents } from "./utils"
export type { AvailableAgent } from "./sisyphus-prompt-builder"
export { createSisyphusAgent } from "./sisyphus"
export { createOracleAgent, ORACLE_PROMPT_METADATA } from "./oracle"
export { createLibrarianAgent, LIBRARIAN_PROMPT_METADATA } from "./librarian"
export { createExploreAgent, EXPLORE_PROMPT_METADATA } from "./explore"
export { createFrontendUiUxEngineerAgent, FRONTEND_PROMPT_METADATA } from "./frontend-ui-ux-engineer"
export { createDocumentWriterAgent, DOCUMENT_WRITER_PROMPT_METADATA } from "./document-writer"
export { createMultimodalLookerAgent, MULTIMODAL_LOOKER_PROMPT_METADATA } from "./multimodal-looker"
export { createMetisAgent, METIS_SYSTEM_PROMPT, metisPromptMetadata } from "./metis"
export { createMomusAgent, MOMUS_SYSTEM_PROMPT, momusPromptMetadata } from "./momus"
export { createOrchestratorSisyphusAgent, orchestratorSisyphusPromptMetadata } from "./orchestrator-sisyphus"
+1 -4
View File
@@ -2,8 +2,6 @@ import type { AgentConfig } from "@opencode-ai/sdk"
import type { AgentPromptMetadata } from "./types"
import { createAgentToolRestrictions } from "../shared/permission-compat"
const DEFAULT_MODEL = "opencode/glm-4.7-free"
export const LIBRARIAN_PROMPT_METADATA: AgentPromptMetadata = {
category: "exploration",
cost: "CHEAP",
@@ -21,7 +19,7 @@ export const LIBRARIAN_PROMPT_METADATA: AgentPromptMetadata = {
],
}
export function createLibrarianAgent(model: string = DEFAULT_MODEL): AgentConfig {
export function createLibrarianAgent(model: string): AgentConfig {
const restrictions = createAgentToolRestrictions([
"write",
"edit",
@@ -326,4 +324,3 @@ grep_app_searchGitHub(query: "useQuery")
}
}
export const librarianAgent = createLibrarianAgent()
+1 -4
View File
@@ -278,9 +278,7 @@ const metisRestrictions = createAgentToolRestrictions([
"delegate_task",
])
const DEFAULT_MODEL = "anthropic/claude-opus-4-5"
export function createMetisAgent(model: string = DEFAULT_MODEL): AgentConfig {
export function createMetisAgent(model: string): AgentConfig {
return {
description:
"Pre-planning consultant that analyzes requests to identify hidden intentions, ambiguities, and AI failure points.",
@@ -293,7 +291,6 @@ export function createMetisAgent(model: string = DEFAULT_MODEL): AgentConfig {
} as AgentConfig
}
export const metisAgent: AgentConfig = createMetisAgent()
export const metisPromptMetadata: AgentPromptMetadata = {
category: "advisor",
+1 -4
View File
@@ -17,8 +17,6 @@ import { createAgentToolRestrictions } from "../shared/permission-compat"
* implementation.
*/
const DEFAULT_MODEL = "openai/gpt-5.2"
export const MOMUS_SYSTEM_PROMPT = `You are a work plan review expert. You review the provided work plan (.sisyphus/plans/{name}.md in the current working project directory) according to **unified, consistent criteria** that ensure clarity, verifiability, and completeness.
**CRITICAL FIRST RULE**:
@@ -391,7 +389,7 @@ Use structured format, **in the same language as the work plan**.
**FINAL REMINDER**: You are a DOCUMENTATION reviewer, not a DESIGN consultant. The author's implementation direction is SACRED. Your job ends at "Is this well-documented enough to execute?" - NOT "Is this the right approach?"
`
export function createMomusAgent(model: string = DEFAULT_MODEL): AgentConfig {
export function createMomusAgent(model: string): AgentConfig {
const restrictions = createAgentToolRestrictions([
"write",
"edit",
@@ -416,7 +414,6 @@ export function createMomusAgent(model: string = DEFAULT_MODEL): AgentConfig {
return { ...base, thinking: { type: "enabled", budgetTokens: 32000 } } as AgentConfig
}
export const momusAgent = createMomusAgent()
export const momusPromptMetadata: AgentPromptMetadata = {
category: "advisor",
+1 -6
View File
@@ -2,8 +2,6 @@ import type { AgentConfig } from "@opencode-ai/sdk"
import type { AgentPromptMetadata } from "./types"
import { createAgentToolAllowlist } from "../shared/permission-compat"
const DEFAULT_MODEL = "google/gemini-3-flash"
export const MULTIMODAL_LOOKER_PROMPT_METADATA: AgentPromptMetadata = {
category: "utility",
cost: "CHEAP",
@@ -11,9 +9,7 @@ export const MULTIMODAL_LOOKER_PROMPT_METADATA: AgentPromptMetadata = {
triggers: [],
}
export function createMultimodalLookerAgent(
model: string = DEFAULT_MODEL
): AgentConfig {
export function createMultimodalLookerAgent(model: string): AgentConfig {
const restrictions = createAgentToolAllowlist(["read"])
return {
@@ -58,4 +54,3 @@ Your output goes straight to the main agent for continued work.`,
}
}
export const multimodalLookerAgent = createMultimodalLookerAgent()
+1 -4
View File
@@ -3,8 +3,6 @@ import type { AgentPromptMetadata } from "./types"
import { isGptModel } from "./types"
import { createAgentToolRestrictions } from "../shared/permission-compat"
const DEFAULT_MODEL = "openai/gpt-5.2"
export const ORACLE_PROMPT_METADATA: AgentPromptMetadata = {
category: "advisor",
cost: "EXPENSIVE",
@@ -97,7 +95,7 @@ Organize your final answer in three tiers:
Your response goes directly to the user with no intermediate processing. Make your final message self-contained: a clear recommendation they can act on immediately, covering both what to do and why.`
export function createOracleAgent(model: string = DEFAULT_MODEL): AgentConfig {
export function createOracleAgent(model: string): AgentConfig {
const restrictions = createAgentToolRestrictions([
"write",
"edit",
@@ -122,4 +120,3 @@ export function createOracleAgent(model: string = DEFAULT_MODEL): AgentConfig {
return { ...base, thinking: { type: "enabled", budgetTokens: 32000 } } as AgentConfig
}
export const oracleAgent = createOracleAgent()
+5 -6
View File
@@ -1458,9 +1458,10 @@ function buildDynamicOrchestratorPrompt(ctx?: OrchestratorContext): string {
.replace("{SKILLS_SECTION}", skillsSection)
}
const DEFAULT_MODEL = "anthropic/claude-sonnet-4-5"
export function createOrchestratorSisyphusAgent(ctx?: OrchestratorContext): AgentConfig {
export function createOrchestratorSisyphusAgent(ctx: OrchestratorContext): AgentConfig {
if (!ctx.model) {
throw new Error("createOrchestratorSisyphusAgent requires a model in context")
}
const restrictions = createAgentToolRestrictions([
"task",
"call_omo_agent",
@@ -1469,7 +1470,7 @@ export function createOrchestratorSisyphusAgent(ctx?: OrchestratorContext): Agen
description:
"Orchestrates work via delegate_task() to complete ALL tasks in a todo list until fully done",
mode: "primary" as const,
model: ctx?.model ?? DEFAULT_MODEL,
model: ctx.model,
temperature: 0.1,
prompt: buildDynamicOrchestratorPrompt(ctx),
thinking: { type: "enabled", budgetTokens: 32000 },
@@ -1478,8 +1479,6 @@ export function createOrchestratorSisyphusAgent(ctx?: OrchestratorContext): Agen
} as AgentConfig
}
export const orchestratorSisyphusAgent: AgentConfig = createOrchestratorSisyphusAgent()
export const orchestratorSisyphusPromptMetadata: AgentPromptMetadata = {
category: "advisor",
cost: "EXPENSIVE",
+1 -1
View File
@@ -138,7 +138,7 @@ export function createSisyphusJuniorAgent(
promptAppend?: string
): AgentConfig {
const prompt = buildSisyphusJuniorPrompt(promptAppend)
const model = categoryConfig.model
const model = categoryConfig.model ?? SISYPHUS_JUNIOR_DEFAULTS.model
const baseRestrictions = createAgentToolRestrictions(BLOCKED_TOOLS)
const categoryPermission = categoryConfig.tools
? Object.fromEntries(
+1 -4
View File
@@ -14,8 +14,6 @@ import {
categorizeTools,
} from "./sisyphus-prompt-builder"
const DEFAULT_MODEL = "anthropic/claude-opus-4-5"
const SISYPHUS_ROLE_SECTION = `<Role>
You are "Sisyphus" - Powerful AI Agent with orchestration capabilities from OhMyOpenCode.
@@ -607,7 +605,7 @@ function buildDynamicSisyphusPrompt(
}
export function createSisyphusAgent(
model: string = DEFAULT_MODEL,
model: string,
availableAgents?: AvailableAgent[],
availableToolNames?: string[],
availableSkills?: AvailableSkill[]
@@ -637,4 +635,3 @@ export function createSisyphusAgent(
return { ...base, thinking: { type: "enabled", budgetTokens: 32000 } }
}
export const sisyphusAgent = createSisyphusAgent()
+1 -1
View File
@@ -1,6 +1,6 @@
import type { AgentConfig } from "@opencode-ai/sdk"
export type AgentFactory = (model?: string) => AgentConfig
export type AgentFactory = (model: string) => AgentConfig
/**
* Agent category for grouping in Sisyphus prompt sections
+44 -22
View File
@@ -2,12 +2,14 @@ import { describe, test, expect } from "bun:test"
import { createBuiltinAgents } from "./utils"
import type { AgentConfig } from "@opencode-ai/sdk"
const TEST_DEFAULT_MODEL = "anthropic/claude-opus-4-5"
describe("createBuiltinAgents with model overrides", () => {
test("Sisyphus with default model has thinking config", () => {
// #given - no overrides
// #given - no overrides, using systemDefaultModel
// #when
const agents = createBuiltinAgents()
const agents = createBuiltinAgents([], {}, undefined, TEST_DEFAULT_MODEL)
// #then
expect(agents.Sisyphus.model).toBe("anthropic/claude-opus-4-5")
@@ -22,7 +24,7 @@ describe("createBuiltinAgents with model overrides", () => {
}
// #when
const agents = createBuiltinAgents([], overrides)
const agents = createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL)
// #then
expect(agents.Sisyphus.model).toBe("github-copilot/gpt-5.2")
@@ -44,10 +46,26 @@ describe("createBuiltinAgents with model overrides", () => {
})
test("Oracle with default model has reasoningEffort", () => {
// #given - no overrides
// #given - no overrides, using systemDefaultModel for other agents
// Oracle uses its own default model (openai/gpt-5.2) from the factory singleton
// #when
const agents = createBuiltinAgents()
const agents = createBuiltinAgents([], {}, undefined, TEST_DEFAULT_MODEL)
// #then - Oracle uses systemDefaultModel since model is now required
expect(agents.oracle.model).toBe("anthropic/claude-opus-4-5")
expect(agents.oracle.thinking).toEqual({ type: "enabled", budgetTokens: 32000 })
expect(agents.oracle.reasoningEffort).toBeUndefined()
})
test("Oracle with GPT model override has reasoningEffort, no thinking", () => {
// #given
const overrides = {
oracle: { model: "openai/gpt-5.2" },
}
// #when
const agents = createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL)
// #then
expect(agents.oracle.model).toBe("openai/gpt-5.2")
@@ -63,7 +81,7 @@ describe("createBuiltinAgents with model overrides", () => {
}
// #when
const agents = createBuiltinAgents([], overrides)
const agents = createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL)
// #then
expect(agents.oracle.model).toBe("anthropic/claude-sonnet-4")
@@ -79,7 +97,7 @@ describe("createBuiltinAgents with model overrides", () => {
}
// #when
const agents = createBuiltinAgents([], overrides)
const agents = createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL)
// #then
expect(agents.Sisyphus.model).toBe("github-copilot/gpt-5.2")
@@ -89,9 +107,10 @@ describe("createBuiltinAgents with model overrides", () => {
describe("buildAgent with category and skills", () => {
const { buildAgent } = require("./utils")
const TEST_MODEL = "anthropic/claude-opus-4-5"
test("agent with category inherits category settings", () => {
// #given
// #given - agent factory that sets category but no model
const source = {
"test-agent": () =>
({
@@ -101,10 +120,11 @@ describe("buildAgent with category and skills", () => {
}
// #when
const agent = buildAgent(source["test-agent"])
const agent = buildAgent(source["test-agent"], TEST_MODEL)
// #then
expect(agent.model).toBe("google/gemini-3-pro-preview")
// #then - DEFAULT_CATEGORIES only has temperature, not model
// Model remains undefined since neither factory nor category provides it
expect(agent.model).toBeUndefined()
expect(agent.temperature).toBe(0.7)
})
@@ -120,7 +140,7 @@ describe("buildAgent with category and skills", () => {
}
// #when
const agent = buildAgent(source["test-agent"])
const agent = buildAgent(source["test-agent"], TEST_MODEL)
// #then
expect(agent.model).toBe("custom/model")
@@ -145,7 +165,7 @@ describe("buildAgent with category and skills", () => {
}
// #when
const agent = buildAgent(source["test-agent"], undefined, categories)
const agent = buildAgent(source["test-agent"], TEST_MODEL, categories)
// #then
expect(agent.model).toBe("openai/gpt-5.2")
@@ -164,7 +184,7 @@ describe("buildAgent with category and skills", () => {
}
// #when
const agent = buildAgent(source["test-agent"])
const agent = buildAgent(source["test-agent"], TEST_MODEL)
// #then
expect(agent.prompt).toContain("Role: Designer-Turned-Developer")
@@ -184,7 +204,7 @@ describe("buildAgent with category and skills", () => {
}
// #when
const agent = buildAgent(source["test-agent"])
const agent = buildAgent(source["test-agent"], TEST_MODEL)
// #then
expect(agent.prompt).toContain("Role: Designer-Turned-Developer")
@@ -204,7 +224,7 @@ describe("buildAgent with category and skills", () => {
}
// #when
const agent = buildAgent(source["test-agent"])
const agent = buildAgent(source["test-agent"], TEST_MODEL)
// #then
expect(agent.model).toBe("custom/model")
@@ -225,10 +245,10 @@ describe("buildAgent with category and skills", () => {
}
// #when
const agent = buildAgent(source["test-agent"])
const agent = buildAgent(source["test-agent"], TEST_MODEL)
// #then
expect(agent.model).toBe("openai/gpt-5.2")
// #then - DEFAULT_CATEGORIES["ultrabrain"] only has temperature, not model
expect(agent.model).toBeUndefined()
expect(agent.temperature).toBe(0.1)
expect(agent.prompt).toContain("Role: Designer-Turned-Developer")
expect(agent.prompt).toContain("Task description")
@@ -246,9 +266,11 @@ describe("buildAgent with category and skills", () => {
}
// #when
const agent = buildAgent(source["test-agent"])
const agent = buildAgent(source["test-agent"], TEST_MODEL)
// #then
// Note: The factory receives model, but if category doesn't exist, it's not applied
// The agent's model comes from the factory output (which doesn't set model)
expect(agent.model).toBeUndefined()
expect(agent.prompt).toBe("Base prompt")
})
@@ -265,7 +287,7 @@ describe("buildAgent with category and skills", () => {
}
// #when
const agent = buildAgent(source["test-agent"])
const agent = buildAgent(source["test-agent"], TEST_MODEL)
// #then
expect(agent.prompt).toContain("Role: Designer-Turned-Developer")
@@ -284,7 +306,7 @@ describe("buildAgent with category and skills", () => {
}
// #when
const agent = buildAgent(source["test-agent"])
const agent = buildAgent(source["test-agent"], TEST_MODEL)
// #then
expect(agent.prompt).toBe("Base prompt")
+10 -4
View File
@@ -9,7 +9,7 @@ import { createFrontendUiUxEngineerAgent, FRONTEND_PROMPT_METADATA } from "./fro
import { createDocumentWriterAgent, DOCUMENT_WRITER_PROMPT_METADATA } from "./document-writer"
import { createMultimodalLookerAgent, MULTIMODAL_LOOKER_PROMPT_METADATA } from "./multimodal-looker"
import { createMetisAgent } from "./metis"
import { createOrchestratorSisyphusAgent, orchestratorSisyphusAgent } from "./orchestrator-sisyphus"
import { createOrchestratorSisyphusAgent } from "./orchestrator-sisyphus"
import { createMomusAgent } from "./momus"
import type { AvailableAgent } from "./sisyphus-prompt-builder"
import { deepMerge } from "../shared"
@@ -28,7 +28,9 @@ const agentSources: Record<BuiltinAgentName, AgentSource> = {
"multimodal-looker": createMultimodalLookerAgent,
"Metis (Plan Consultant)": createMetisAgent,
"Momus (Plan Reviewer)": createMomusAgent,
"orchestrator-sisyphus": orchestratorSisyphusAgent,
// Note: orchestrator-sisyphus is handled specially in createBuiltinAgents()
// because it needs OrchestratorContext, not just a model string
"orchestrator-sisyphus": createOrchestratorSisyphusAgent as unknown as AgentFactory,
}
/**
@@ -50,7 +52,7 @@ function isFactory(source: AgentSource): source is AgentFactory {
export function buildAgent(
source: AgentSource,
model?: string,
model: string,
categories?: CategoriesConfig,
gitMasterConfig?: GitMasterConfig
): AgentConfig {
@@ -134,6 +136,10 @@ export function createBuiltinAgents(
categories?: CategoriesConfig,
gitMasterConfig?: GitMasterConfig
): Record<string, AgentConfig> {
if (!systemDefaultModel) {
throw new Error("createBuiltinAgents requires systemDefaultModel")
}
const result: Record<string, AgentConfig> = {}
const availableAgents: AvailableAgent[] = []
@@ -149,7 +155,7 @@ export function createBuiltinAgents(
if (disabledAgents.includes(agentName)) continue
const override = agentOverrides[agentName]
const model = override?.model
const model = override?.model ?? systemDefaultModel
let config = buildAgent(source, model, mergedCategories, gitMasterConfig)