From d7bc817b7557a031300466b8ae5783aab5cad8e1 Mon Sep 17 00:00:00 2001 From: Christopher Tso Date: Sun, 21 Dec 2025 19:09:26 +1100 Subject: [PATCH] feat: auto-detect model provider and apply appropriate options (#146) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When overriding an agent's model to a different provider, the agent now automatically gets provider-appropriate reasoning options: - GPT models: `reasoningEffort`, `textVerbosity` - Anthropic models: `thinking` with `budgetTokens` ## Why utils.ts changes are required The original flow merges overrides onto pre-built agent configs: mergeAgentConfig(sisyphusAgent, { model: "gpt-5.2" }) // Result: { model: "gpt-5.2", thinking: {...} } The `thinking` config persists because it exists in the pre-built `sisyphusAgent`. GPT models ignore `thinking` and need `reasoningEffort`. The fix: call the agent factory with the resolved model, so the factory can return the correct provider-specific config: buildAgent(createSisyphusAgent, "gpt-5.2") // Result: { model: "gpt-5.2", reasoningEffort: "medium" } Closes #144 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.5 --- src/agents/oracle.ts | 35 +++++++++++----- src/agents/sisyphus.ts | 33 +++++++++------ src/agents/types.ts | 6 +++ src/agents/utils.test.ts | 87 ++++++++++++++++++++++++++++++++++++++++ src/agents/utils.ts | 50 ++++++++++++----------- 5 files changed, 164 insertions(+), 47 deletions(-) create mode 100644 src/agents/utils.test.ts diff --git a/src/agents/oracle.ts b/src/agents/oracle.ts index 391977c..f37241f 100644 --- a/src/agents/oracle.ts +++ b/src/agents/oracle.ts @@ -1,15 +1,9 @@ import type { AgentConfig } from "@opencode-ai/sdk" +import { isGptModel } from "./types" -export const oracleAgent: AgentConfig = { - description: - "Expert technical advisor with deep reasoning for architecture decisions, code analysis, and engineering guidance.", - mode: "subagent", - model: "openai/gpt-5.2", - temperature: 0.1, - reasoningEffort: "medium", - textVerbosity: "high", - tools: { write: false, edit: false, task: false, background_task: false }, - prompt: `You are a strategic technical advisor with deep reasoning capabilities, operating as a specialized consultant within an AI-assisted development environment. +const DEFAULT_MODEL = "openai/gpt-5.2" + +const ORACLE_SYSTEM_PROMPT = `You are a strategic technical advisor with deep reasoning capabilities, operating as a specialized consultant within an AI-assisted development environment. ## Context @@ -73,5 +67,24 @@ Organize your final answer in three tiers: ## Critical Note -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.`, +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 { + const base = { + description: + "Expert technical advisor with deep reasoning for architecture decisions, code analysis, and engineering guidance.", + mode: "subagent" as const, + model, + temperature: 0.1, + tools: { write: false, edit: false, task: false, background_task: false }, + prompt: ORACLE_SYSTEM_PROMPT, + } + + if (isGptModel(model)) { + return { ...base, reasoningEffort: "medium", textVerbosity: "high" } + } + + return { ...base, thinking: { type: "enabled", budgetTokens: 32000 } } } + +export const oracleAgent = createOracleAgent() diff --git a/src/agents/sisyphus.ts b/src/agents/sisyphus.ts index e77552a..5528154 100644 --- a/src/agents/sisyphus.ts +++ b/src/agents/sisyphus.ts @@ -1,4 +1,7 @@ import type { AgentConfig } from "@opencode-ai/sdk" +import { isGptModel } from "./types" + +const DEFAULT_MODEL = "anthropic/claude-opus-4-5" const SISYPHUS_SYSTEM_PROMPT = ` You are "Sisyphus" - Powerful AI Agent with orchestration capabilities from OhMyOpenCode. @@ -452,16 +455,22 @@ If the user's approach seems problematic: ` -export const sisyphusAgent: AgentConfig = { - description: - "Sisyphus - Powerful AI orchestrator from OhMyOpenCode. Plans obsessively with todos, assesses search complexity before exploration, delegates strategically to specialized agents. Uses explore for internal code (parallel-friendly), librarian only for external docs, and always delegates UI work to frontend engineer.", - mode: "primary", - model: "anthropic/claude-opus-4-5", - thinking: { - type: "enabled", - budgetTokens: 32000, - }, - maxTokens: 64000, - prompt: SISYPHUS_SYSTEM_PROMPT, - color: "#00CED1", +export function createSisyphusAgent(model: string = DEFAULT_MODEL): AgentConfig { + const base = { + description: + "Sisyphus - Powerful AI orchestrator from OhMyOpenCode. Plans obsessively with todos, assesses search complexity before exploration, delegates strategically to specialized agents. Uses explore for internal code (parallel-friendly), librarian only for external docs, and always delegates UI work to frontend engineer.", + mode: "primary" as const, + model, + maxTokens: 64000, + prompt: SISYPHUS_SYSTEM_PROMPT, + color: "#00CED1", + } + + if (isGptModel(model)) { + return { ...base, reasoningEffort: "medium" } + } + + return { ...base, thinking: { type: "enabled", budgetTokens: 32000 } } } + +export const sisyphusAgent = createSisyphusAgent() diff --git a/src/agents/types.ts b/src/agents/types.ts index 8cf7cdd..7484d0e 100644 --- a/src/agents/types.ts +++ b/src/agents/types.ts @@ -1,5 +1,11 @@ import type { AgentConfig } from "@opencode-ai/sdk" +export type AgentFactory = (model?: string) => AgentConfig + +export function isGptModel(model: string): boolean { + return model.startsWith("openai/") || model.startsWith("github-copilot/gpt-") +} + export type BuiltinAgentName = | "Sisyphus" | "oracle" diff --git a/src/agents/utils.test.ts b/src/agents/utils.test.ts new file mode 100644 index 0000000..4c48275 --- /dev/null +++ b/src/agents/utils.test.ts @@ -0,0 +1,87 @@ +import { describe, test, expect } from "bun:test" +import { createBuiltinAgents } from "./utils" + +describe("createBuiltinAgents with model overrides", () => { + test("Sisyphus with default model has thinking config", () => { + // #given - no overrides + + // #when + const agents = createBuiltinAgents() + + // #then + 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("Sisyphus with GPT model override has reasoningEffort, no thinking", () => { + // #given + const overrides = { + Sisyphus: { model: "github-copilot/gpt-5.2" }, + } + + // #when + const agents = createBuiltinAgents([], overrides) + + // #then + expect(agents.Sisyphus.model).toBe("github-copilot/gpt-5.2") + expect(agents.Sisyphus.reasoningEffort).toBe("medium") + expect(agents.Sisyphus.thinking).toBeUndefined() + }) + + test("Sisyphus with systemDefaultModel GPT has reasoningEffort, no thinking", () => { + // #given + const systemDefaultModel = "openai/gpt-5.2" + + // #when + const agents = createBuiltinAgents([], {}, undefined, systemDefaultModel) + + // #then + expect(agents.Sisyphus.model).toBe("openai/gpt-5.2") + expect(agents.Sisyphus.reasoningEffort).toBe("medium") + expect(agents.Sisyphus.thinking).toBeUndefined() + }) + + test("Oracle with default model has reasoningEffort", () => { + // #given - no overrides + + // #when + const agents = createBuiltinAgents() + + // #then + 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() + }) + + test("Oracle with Claude model override has thinking, no reasoningEffort", () => { + // #given + const overrides = { + oracle: { model: "anthropic/claude-sonnet-4" }, + } + + // #when + const agents = createBuiltinAgents([], overrides) + + // #then + expect(agents.oracle.model).toBe("anthropic/claude-sonnet-4") + expect(agents.oracle.thinking).toEqual({ type: "enabled", budgetTokens: 32000 }) + expect(agents.oracle.reasoningEffort).toBeUndefined() + expect(agents.oracle.textVerbosity).toBeUndefined() + }) + + test("non-model overrides are still applied after factory rebuild", () => { + // #given + const overrides = { + Sisyphus: { model: "github-copilot/gpt-5.2", temperature: 0.5 }, + } + + // #when + const agents = createBuiltinAgents([], overrides) + + // #then + expect(agents.Sisyphus.model).toBe("github-copilot/gpt-5.2") + expect(agents.Sisyphus.temperature).toBe(0.5) + }) +}) diff --git a/src/agents/utils.ts b/src/agents/utils.ts index cca863a..feb3fad 100644 --- a/src/agents/utils.ts +++ b/src/agents/utils.ts @@ -1,7 +1,7 @@ import type { AgentConfig } from "@opencode-ai/sdk" -import type { BuiltinAgentName, AgentOverrideConfig, AgentOverrides } from "./types" -import { sisyphusAgent } from "./sisyphus" -import { oracleAgent } from "./oracle" +import type { BuiltinAgentName, AgentOverrideConfig, AgentOverrides, AgentFactory } from "./types" +import { createSisyphusAgent } from "./sisyphus" +import { createOracleAgent } from "./oracle" import { librarianAgent } from "./librarian" import { exploreAgent } from "./explore" import { frontendUiUxEngineerAgent } from "./frontend-ui-ux-engineer" @@ -9,9 +9,11 @@ import { documentWriterAgent } from "./document-writer" import { multimodalLookerAgent } from "./multimodal-looker" import { deepMerge } from "../shared" -const allBuiltinAgents: Record = { - Sisyphus: sisyphusAgent, - oracle: oracleAgent, +type AgentSource = AgentFactory | AgentConfig + +const agentSources: Record = { + Sisyphus: createSisyphusAgent, + oracle: createOracleAgent, librarian: librarianAgent, explore: exploreAgent, "frontend-ui-ux-engineer": frontendUiUxEngineerAgent, @@ -19,6 +21,14 @@ const allBuiltinAgents: Record = { "multimodal-looker": multimodalLookerAgent, } +function isFactory(source: AgentSource): source is AgentFactory { + return typeof source === "function" +} + +function buildAgent(source: AgentSource, model?: string): AgentConfig { + return isFactory(source) ? source(model) : source +} + export function createEnvContext(directory: string): string { const now = new Date() const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone @@ -67,37 +77,29 @@ export function createBuiltinAgents( ): Record { const result: Record = {} - for (const [name, config] of Object.entries(allBuiltinAgents)) { + for (const [name, source] of Object.entries(agentSources)) { const agentName = name as BuiltinAgentName if (disabledAgents.includes(agentName)) { continue } - let finalConfig = config + const override = agentOverrides[agentName] + const model = override?.model ?? (agentName === "Sisyphus" ? systemDefaultModel : undefined) + + let config = buildAgent(source, model) if ((agentName === "Sisyphus" || agentName === "librarian") && directory && config.prompt) { const envContext = createEnvContext(directory) - finalConfig = { - ...config, - prompt: config.prompt + envContext, - } - } - - const override = agentOverrides[agentName] - - if (agentName === "Sisyphus" && systemDefaultModel && !override?.model) { - finalConfig = { - ...finalConfig, - model: systemDefaultModel, - } + config = { ...config, prompt: config.prompt + envContext } } if (override) { - result[name] = mergeAgentConfig(finalConfig, override) - } else { - result[name] = finalConfig + const { model: _, ...restOverride } = override + config = mergeAgentConfig(config, restOverride) } + + result[name] = config } return result