diff --git a/.opencode/agent/duplicate-pr.md b/.opencode/agent/duplicate-pr.md index c9c932ef790..6f34b7c6c65 100644 --- a/.opencode/agent/duplicate-pr.md +++ b/.opencode/agent/duplicate-pr.md @@ -3,6 +3,7 @@ mode: primary hidden: true model: opencode/claude-haiku-4-5 color: "#E67E22" +variant: "high" tools: "*": false "github-pr-search": true diff --git a/.opencode/agent/triage.md b/.opencode/agent/triage.md index 5d1147a8859..1775a4c3166 100644 --- a/.opencode/agent/triage.md +++ b/.opencode/agent/triage.md @@ -3,6 +3,7 @@ mode: primary hidden: true model: opencode/claude-haiku-4-5 color: "#44BA81" +variant: "high" tools: "*": false "github-triage": true diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 2f85652a93e..595195019f1 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -1066,7 +1066,7 @@ export const PromptInput: Component = (props) => { providerID: currentModel.provider.id, } const agent = currentAgent.name - const variant = local.model.variant.current() + const variant = local.model.variant.effective() const clearInput = () => { prompt.reset() @@ -1604,7 +1604,7 @@ export const PromptInput: Component = (props) => { class="text-text-base _hidden group-hover/prompt-input:inline-block capitalize text-12-regular" onClick={() => local.model.variant.cycle()} > - {local.model.variant.current() ?? "Default"} + {local.model.variant.effective() ?? "Default"} diff --git a/packages/app/src/context/local.tsx b/packages/app/src/context/local.tsx index 2ed57234f29..9afd390e1ab 100644 --- a/packages/app/src/context/local.tsx +++ b/packages/app/src/context/local.tsx @@ -294,6 +294,9 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ const key = `${m.provider.id}/${m.id}` return store.variant?.[key] }, + effective() { + return this.current() ?? agent.current()?.variant + }, list() { const m = current() if (!m) return [] @@ -313,12 +316,12 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ cycle() { const variants = this.list() if (variants.length === 0) return - const currentVariant = this.current() - if (!currentVariant) { + const effective = this.effective() + if (!effective) { this.set(variants[0]) return } - const index = variants.indexOf(currentVariant) + const index = variants.indexOf(effective) if (index === -1 || index === variants.length - 1) { this.set(undefined) return diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index 64875091916..fe9583b7b45 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -35,6 +35,7 @@ export namespace Agent { }) .optional(), prompt: z.string().optional(), + variant: z.string().optional(), options: z.record(z.string(), z.any()), steps: z.number().int().positive().optional(), }) @@ -209,6 +210,7 @@ export namespace Agent { native: false, } if (value.model) item.model = Provider.parseModel(value.model) + if (value.variant) item.variant = value.variant item.prompt = value.prompt ?? item.prompt item.description = value.description ?? item.description item.temperature = value.temperature ?? item.temperature diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index 54248f96f3d..731fa6d9a30 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -89,7 +89,7 @@ export const RunCommand = cmd({ }) .option("variant", { type: "string", - describe: "model variant (provider-specific reasoning effort, e.g., high, max, minimal)", + describe: "model variant (provider-specific reasoning effort, e.g., none, low, medium, high, xhigh, max)", }) }, handler: async (args) => { diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 1fea3f4b305..afb08efc14f 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -252,6 +252,9 @@ function App() { }) local.model.set({ providerID, modelID }, { recent: true }) } + if (args.variant) { + local.model.variant.set(args.variant) + } if (args.sessionID) { route.navigate({ type: "session", diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-agent.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-agent.tsx index 365a22445b4..d5312bd44b4 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-agent.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-agent.tsx @@ -9,10 +9,12 @@ export function DialogAgent() { const options = createMemo(() => local.agent.list().map((item) => { + const desc = item.native ? "native" : item.description + const variant = item.variant ? ` (${item.variant})` : "" return { value: item.name, title: item.name, - description: item.native ? "native" : item.description, + description: desc ? desc + variant : variant.trim(), } }), ) diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index 9ad85d08f0e..19db7e9073e 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -695,8 +695,8 @@ export function Prompt(props: PromptProps) { const showVariant = createMemo(() => { const variants = local.model.variant.list() if (variants.length === 0) return false - const current = local.model.variant.current() - return !!current + const effective = local.model.variant.effective() + return !!effective }) const spinnerDef = createMemo(() => { @@ -945,7 +945,7 @@ export function Prompt(props: PromptProps) { · - {local.model.variant.current()} + {local.model.variant.effective()} diff --git a/packages/opencode/src/cli/cmd/tui/context/args.tsx b/packages/opencode/src/cli/cmd/tui/context/args.tsx index ffd43009a41..b38da4c3c0f 100644 --- a/packages/opencode/src/cli/cmd/tui/context/args.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/args.tsx @@ -3,6 +3,7 @@ import { createSimpleContext } from "./helper" export interface Args { model?: string agent?: string + variant?: string prompt?: string continue?: boolean sessionID?: string diff --git a/packages/opencode/src/cli/cmd/tui/context/local.tsx b/packages/opencode/src/cli/cmd/tui/context/local.tsx index 63f1d9743bf..701f0768bf0 100644 --- a/packages/opencode/src/cli/cmd/tui/context/local.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/local.tsx @@ -314,6 +314,9 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ const key = `${m.providerID}/${m.modelID}` return modelStore.variant[key] }, + effective() { + return this.current() ?? agent.current().variant + }, list() { const m = currentModel() if (!m) return [] @@ -332,12 +335,12 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ cycle() { const variants = this.list() if (variants.length === 0) return - const current = this.current() - if (!current) { + const effective = this.effective() + if (!effective) { this.set(variants[0]) return } - const index = variants.indexOf(current) + const index = variants.indexOf(effective) if (index === -1 || index === variants.length - 1) { this.set(undefined) return diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index d91363954a1..28a47cc219f 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -1244,7 +1244,13 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las ▣{" "} {" "} {Locale.titlecase(props.message.mode)} - · {props.message.modelID} + + {" "} + · {props.message.providerID}/{props.message.modelID} + + + · {props.message.variant} + · {Locale.duration(duration())} diff --git a/packages/opencode/src/cli/cmd/tui/thread.ts b/packages/opencode/src/cli/cmd/tui/thread.ts index 05714268545..ea115fabda6 100644 --- a/packages/opencode/src/cli/cmd/tui/thread.ts +++ b/packages/opencode/src/cli/cmd/tui/thread.ts @@ -71,6 +71,10 @@ export const TuiThreadCommand = cmd({ .option("agent", { type: "string", describe: "agent to use", + }) + .option("variant", { + type: "string", + describe: "model variant (provider-specific reasoning effort, e.g., none, low, medium, high, xhigh, max)", }), handler: async (args) => { // Resolve relative paths against PWD to preserve behavior when using --cwd flag @@ -149,6 +153,7 @@ export const TuiThreadCommand = cmd({ sessionID: args.session, agent: args.agent, model: args.model, + variant: args.variant, prompt, }, onExit: async () => { diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index bf4a6035bd8..5bf56a12249 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -527,6 +527,7 @@ export namespace Config { export const Agent = z .object({ model: z.string().optional(), + variant: z.string().optional().describe("Default variant to use for this agent"), temperature: z.number().optional(), top_p: z.number().optional(), prompt: z.string().optional(), @@ -558,6 +559,7 @@ export namespace Config { const knownKeys = new Set([ "name", "model", + "variant", "prompt", "description", "temperature", diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index ebc22637e10..fb9d31d44e3 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -91,8 +91,8 @@ export namespace LLM { system.push(header, rest.join("\n")) } - const variant = - !input.small && input.model.variants && input.user.variant ? input.model.variants[input.user.variant] : {} + const selectedVariant = input.user.variant ?? input.agent.variant + const variant = !input.small && input.model.variants && selectedVariant ? input.model.variants[selectedVariant] : {} const base = input.small ? ProviderTransform.smallOptions(input.model) : ProviderTransform.options(input.model, input.sessionID, provider.options) diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index c1d4015f6d3..c6396256990 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -379,6 +379,7 @@ export namespace MessageV2 { }), }), finish: z.string().optional(), + variant: z.string().optional(), }).meta({ ref: "AssistantMessage", }) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 345b1c49e65..023a942becc 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -336,6 +336,7 @@ export namespace SessionPrompt { }, modelID: model.id, providerID: model.providerID, + variant: lastUser.variant, time: { created: Date.now(), }, @@ -536,6 +537,7 @@ export namespace SessionPrompt { }, modelID: model.id, providerID: model.providerID, + variant: lastUser.variant, time: { created: Date.now(), }, @@ -830,7 +832,7 @@ export namespace SessionPrompt { agent: agent.name, model: input.model ?? agent.model ?? (await lastModel(input.sessionID)), system: input.system, - variant: input.variant, + variant: input.variant ?? agent.variant, } const parts = await Promise.all( diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts index 170d4448088..93ff97c6f95 100644 --- a/packages/opencode/src/tool/task.ts +++ b/packages/opencode/src/tool/task.ts @@ -18,6 +18,8 @@ const parameters = z.object({ subagent_type: z.string().describe("The type of specialized agent to use for this task"), session_id: z.string().describe("Existing Task session to continue").optional(), command: z.string().describe("The command that triggered this task").optional(), + model: z.string().describe("Override the subagent's model (format: providerID/modelID)").optional(), + variant: z.string().describe("Override the subagent's variant (e.g. 'low', 'medium', 'high')").optional(), }) export const TaskTool = Tool.define("task", async (ctx) => { @@ -130,10 +132,23 @@ export const TaskTool = Tool.define("task", async (ctx) => { }) }) - const model = agent.model ?? { - modelID: msg.info.modelID, - providerID: msg.info.providerID, - } + // Model priority: param override > agent config > parent context + const model = (() => { + if (params.model) { + const [providerID, ...rest] = params.model.split("/") + const modelID = rest.join("/") + if (!providerID || !modelID) { + throw new Error(`Invalid model format: ${params.model}. Expected: providerID/modelID`) + } + return { providerID, modelID } + } + return ( + agent.model ?? { + modelID: msg.info.modelID, + providerID: msg.info.providerID, + } + ) + })() function cancel() { SessionPrompt.cancel(session.id) @@ -150,6 +165,10 @@ export const TaskTool = Tool.define("task", async (ctx) => { providerID: model.providerID, }, agent: agent.name, + // Variant priority: param override > subagent's configured variant > undefined. + // Model priority: param override > subagent's configured model > parent's model. + // This allows orchestrator agents to dynamically assign model+variant per task. + variant: params.variant ?? agent.variant, tools: { todowrite: false, todoread: false, diff --git a/packages/opencode/src/tool/task.txt b/packages/opencode/src/tool/task.txt index 7af2a6f60dd..ddec2fb11a3 100644 --- a/packages/opencode/src/tool/task.txt +++ b/packages/opencode/src/tool/task.txt @@ -5,6 +5,12 @@ Available agent types and the tools they have access to: When using the Task tool, you must specify a subagent_type parameter to select which agent type to use. +Optional override parameters: +- model: Override the subagent's default model (format: providerID/modelID, e.g. "anthropic/claude-haiku-4-5") +- variant: Override the subagent's default variant (e.g. "low", "medium", "high") + +These overrides take priority over the agent's configured defaults, allowing dynamic model/variant selection per task. + When to use the Task tool: - When you are instructed to execute custom slash commands. Use the Task tool with the slash command invocation as the entire prompt. The slash command can take arguments. For example: Task(description="Check the file", prompt="/check-file path/to/file.py") diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 2bb3a600274..dfd95f2a7a1 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -169,6 +169,7 @@ export type AssistantMessage = { } } finish?: string + variant?: string } export type Message = UserMessage | AssistantMessage @@ -1320,6 +1321,10 @@ export type PermissionConfig = export type AgentConfig = { model?: string + /** + * Default variant to use for this agent + */ + variant?: string temperature?: number top_p?: number prompt?: string @@ -2006,6 +2011,7 @@ export type Agent = { providerID: string } prompt?: string + variant?: string options: { [key: string]: unknown } diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 107d461f380..c00f109b747 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -6077,6 +6077,9 @@ }, "finish": { "type": "string" + }, + "variant": { + "type": "string" } }, "required": [ @@ -8670,6 +8673,10 @@ "model": { "type": "string" }, + "variant": { + "description": "Default variant to use for this agent", + "type": "string" + }, "temperature": { "type": "number" }, @@ -10307,6 +10314,9 @@ "prompt": { "type": "string" }, + "variant": { + "type": "string" + }, "options": { "type": "object", "propertyNames": {