diff --git a/.opencode/agent/triage.md b/.opencode/agent/triage.md
index a77b92737bc9..b2f1bdffa56f 100644
--- a/.opencode/agent/triage.md
+++ b/.opencode/agent/triage.md
@@ -3,6 +3,7 @@ mode: primary
hidden: true
model: opencode/minimax-m2.5
color: "#44BA81"
+variant: "high"
tools:
"*": false
"github-triage": true
diff --git a/packages/app/src/context/local.tsx b/packages/app/src/context/local.tsx
index 84a613c0d285..142aac2a63c7 100644
--- a/packages/app/src/context/local.tsx
+++ b/packages/app/src/context/local.tsx
@@ -321,6 +321,9 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
configured: this.configured(),
})
},
+ effective() {
+ return this.current()
+ },
list() {
const item = current()
if (!item?.variants) return []
diff --git a/packages/app/src/context/model-variant.test.ts b/packages/app/src/context/model-variant.test.ts
index 583bc5c3dc71..7418e5034df6 100644
--- a/packages/app/src/context/model-variant.test.ts
+++ b/packages/app/src/context/model-variant.test.ts
@@ -54,17 +54,17 @@ describe("model variant", () => {
expect(value).toBeUndefined()
})
- test("cycles from configured variant to next", () => {
+ test("starts cycling from first variant when no explicit selection", () => {
const value = cycleModelVariant({
variants: ["low", "high", "xhigh"],
selected: undefined,
configured: "high",
})
- expect(value).toBe("xhigh")
+ expect(value).toBe("low")
})
- test("wraps from configured last variant to first", () => {
+ test("starts from first even when configured is last", () => {
const value = cycleModelVariant({
variants: ["low", "high", "xhigh"],
selected: undefined,
@@ -83,4 +83,17 @@ describe("model variant", () => {
expect(value).toBe("low")
})
+
+ test("cycles through all variants from explicit selection", () => {
+ const variants = ["low", "high", "xhigh"]
+ const first = cycleModelVariant({ variants, selected: undefined, configured: "high" })
+ const second = cycleModelVariant({ variants, selected: first, configured: "high" })
+ const third = cycleModelVariant({ variants, selected: second, configured: "high" })
+ const fourth = cycleModelVariant({ variants, selected: third, configured: "high" })
+
+ expect(first).toBe("low")
+ expect(second).toBe("high")
+ expect(third).toBe("xhigh")
+ expect(fourth).toBeUndefined()
+ })
})
diff --git a/packages/app/src/context/model-variant.ts b/packages/app/src/context/model-variant.ts
index 525acbba3219..460f5b553195 100644
--- a/packages/app/src/context/model-variant.ts
+++ b/packages/app/src/context/model-variant.ts
@@ -43,10 +43,5 @@ export function cycleModelVariant(input: VariantInput) {
if (index === input.variants.length - 1) return undefined
return input.variants[index + 1]
}
- if (input.configured && input.variants.includes(input.configured)) {
- const index = input.variants.indexOf(input.configured)
- if (index === input.variants.length - 1) return input.variants[0]
- return input.variants[index + 1]
- }
return input.variants[0]
}
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 96563b884ede..f1c713b7bdbd 100644
--- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
@@ -35,6 +35,7 @@ import { useToast } from "../../ui/toast"
import { useKV } from "../../context/kv"
import { useTextareaKeybindings } from "../textarea-keybindings"
import { DialogSkill } from "../dialog-skill"
+import { getConfiguredAgentVariant } from "@tui/context/model-variant"
export type PromptProps = {
sessionID?: string
@@ -197,7 +198,20 @@ export function Prompt(props: PromptProps) {
if (msg.agent && isPrimaryAgent) {
local.agent.set(msg.agent)
if (msg.model) local.model.set(msg.model)
- if (msg.variant) local.model.variant.set(msg.variant)
+ if (msg.variant) {
+ const info = local.agent.list().find((x) => x.name === msg.agent)
+ const provider = msg.model ? sync.data.provider.find((x) => x.id === msg.model.providerID) : undefined
+ const model = msg.model ? provider?.models[msg.model.modelID] : undefined
+ const configured = getConfiguredAgentVariant({
+ agent: { model: info?.model, variant: info?.variant },
+ model: msg.model
+ ? { providerID: msg.model.providerID, modelID: msg.model.modelID, variants: model?.variants }
+ : undefined,
+ })
+ if (msg.variant === configured) local.model.variant.set(undefined)
+ if (msg.variant !== configured) local.model.variant.set(msg.variant)
+ }
+ if (!msg.variant) local.model.variant.set(undefined)
}
}
})
@@ -805,8 +819,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 placeholderText = createMemo(() => {
@@ -1073,7 +1087,7 @@ export function Prompt(props: PromptProps) {
ยท
- {local.model.variant.current()}
+ {local.model.variant.effective()}
diff --git a/packages/opencode/src/cli/cmd/tui/context/local.tsx b/packages/opencode/src/cli/cmd/tui/context/local.tsx
index ec3931b209ad..3bf6eb0234c0 100644
--- a/packages/opencode/src/cli/cmd/tui/context/local.tsx
+++ b/packages/opencode/src/cli/cmd/tui/context/local.tsx
@@ -13,6 +13,7 @@ import { useArgs } from "./args"
import { useSDK } from "./sdk"
import { RGBA } from "@opentui/core"
import { Filesystem } from "@/util/filesystem"
+import { cycleModelVariant, getConfiguredAgentVariant, resolveModelVariant } from "./model-variant"
export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
name: "Local",
@@ -321,6 +322,17 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
})
},
variant: {
+ configured() {
+ const a = agent.current()
+ const m = currentModel()
+ if (!m) return undefined
+ const provider = sync.data.provider.find((x) => x.id === m.providerID)
+ const info = provider?.models[m.modelID]
+ return getConfiguredAgentVariant({
+ agent: { model: a.model, variant: a.variant },
+ model: { providerID: m.providerID, modelID: m.modelID, variants: info?.variants },
+ })
+ },
selected() {
const m = currentModel()
if (!m) return undefined
@@ -328,10 +340,14 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
return modelStore.variant[key]
},
current() {
- const v = this.selected()
- if (!v) return undefined
- if (!this.list().includes(v)) return undefined
- return v
+ return resolveModelVariant({
+ variants: this.list(),
+ selected: this.selected(),
+ configured: this.configured(),
+ })
+ },
+ effective() {
+ return this.current()
},
list() {
const m = currentModel()
@@ -351,17 +367,13 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
cycle() {
const variants = this.list()
if (variants.length === 0) return
- const current = this.current()
- if (!current) {
- this.set(variants[0])
- return
- }
- const index = variants.indexOf(current)
- if (index === -1 || index === variants.length - 1) {
- this.set(undefined)
- return
- }
- this.set(variants[index + 1])
+ this.set(
+ cycleModelVariant({
+ variants,
+ selected: this.selected(),
+ configured: this.configured(),
+ }),
+ )
},
},
}
diff --git a/packages/opencode/src/cli/cmd/tui/context/model-variant.ts b/packages/opencode/src/cli/cmd/tui/context/model-variant.ts
new file mode 100644
index 000000000000..da80b82f44d2
--- /dev/null
+++ b/packages/opencode/src/cli/cmd/tui/context/model-variant.ts
@@ -0,0 +1,47 @@
+type AgentModel = {
+ providerID: string
+ modelID: string
+}
+
+type Agent = {
+ model?: AgentModel
+ variant?: string
+}
+
+type Model = AgentModel & {
+ variants?: Record
+}
+
+type VariantInput = {
+ variants: string[]
+ selected: string | undefined
+ configured: string | undefined
+}
+
+export function getConfiguredAgentVariant(input: { agent: Agent | undefined; model: Model | undefined }) {
+ if (!input.agent?.variant) return undefined
+ if (!input.agent.model) return undefined
+ if (!input.model?.variants) return undefined
+ if (input.agent.model.providerID !== input.model.providerID) return undefined
+ if (input.agent.model.modelID !== input.model.modelID) return undefined
+ if (!(input.agent.variant in input.model.variants)) return undefined
+ return input.agent.variant
+}
+
+export function resolveModelVariant(input: VariantInput) {
+ if (input.selected === "default") return undefined
+ if (input.selected && input.variants.includes(input.selected)) return input.selected
+ if (input.configured && input.variants.includes(input.configured)) return input.configured
+ return undefined
+}
+
+export function cycleModelVariant(input: VariantInput) {
+ if (input.variants.length === 0) return undefined
+ if (input.selected === "default") return input.variants[0]
+ if (input.selected && input.variants.includes(input.selected)) {
+ const index = input.variants.indexOf(input.selected)
+ if (index === input.variants.length - 1) return undefined
+ return input.variants[index + 1]
+ }
+ return input.variants[0]
+}
diff --git a/packages/opencode/test/agent/agent.test.ts b/packages/opencode/test/agent/agent.test.ts
index 98a0fd4c6ec6..bf0ce6cdfc52 100644
--- a/packages/opencode/test/agent/agent.test.ts
+++ b/packages/opencode/test/agent/agent.test.ts
@@ -715,3 +715,31 @@ test("defaultAgent throws when all primary agents are disabled", async () => {
},
})
})
+
+test("agent variant can be set from config", async () => {
+ await using tmp = await tmpdir({
+ config: {
+ agent: {
+ build: { variant: "high" },
+ },
+ },
+ })
+ await Instance.provide({
+ directory: tmp.path,
+ fn: async () => {
+ const build = await Agent.get("build")
+ expect(build?.variant).toBe("high")
+ },
+ })
+})
+
+test("agent variant defaults to undefined when not set", async () => {
+ await using tmp = await tmpdir()
+ await Instance.provide({
+ directory: tmp.path,
+ fn: async () => {
+ const build = await Agent.get("build")
+ expect(build?.variant).toBeUndefined()
+ },
+ })
+})
diff --git a/packages/opencode/test/cli/tui/model-variant.test.ts b/packages/opencode/test/cli/tui/model-variant.test.ts
new file mode 100644
index 000000000000..c2fe0ce1367c
--- /dev/null
+++ b/packages/opencode/test/cli/tui/model-variant.test.ts
@@ -0,0 +1,69 @@
+import { describe, expect, test } from "bun:test"
+import {
+ cycleModelVariant,
+ getConfiguredAgentVariant,
+ resolveModelVariant,
+} from "../../../src/cli/cmd/tui/context/model-variant"
+
+describe("tui model variant", () => {
+ test("resolves configured variant when active model matches", () => {
+ const value = getConfiguredAgentVariant({
+ agent: { model: { providerID: "openai", modelID: "gpt-5.2" }, variant: "high" },
+ model: { providerID: "openai", modelID: "gpt-5.2", variants: { low: {}, high: {} } },
+ })
+
+ expect(value).toBe("high")
+ })
+
+ test("ignores configured variant when active model does not match", () => {
+ const value = getConfiguredAgentVariant({
+ agent: { model: { providerID: "openai", modelID: "gpt-5.2" }, variant: "high" },
+ model: { providerID: "anthropic", modelID: "claude-sonnet-4", variants: { low: {}, high: {} } },
+ })
+
+ expect(value).toBeUndefined()
+ })
+
+ test("prefers selected variant over configured variant", () => {
+ const value = resolveModelVariant({
+ variants: ["low", "high", "xhigh"],
+ selected: "xhigh",
+ configured: "high",
+ })
+
+ expect(value).toBe("xhigh")
+ })
+
+ test("treats default sentinel as explicit default", () => {
+ const value = resolveModelVariant({
+ variants: ["low", "high", "xhigh"],
+ selected: "default",
+ configured: "high",
+ })
+
+ expect(value).toBeUndefined()
+ })
+
+ test("cycles from default sentinel to first variant", () => {
+ const value = cycleModelVariant({
+ variants: ["low", "high", "xhigh"],
+ selected: "default",
+ configured: "high",
+ })
+
+ expect(value).toBe("low")
+ })
+
+ test("cycles through all variants from explicit selection", () => {
+ const variants = ["low", "high", "xhigh"]
+ const first = cycleModelVariant({ variants, selected: undefined, configured: "high" })
+ const second = cycleModelVariant({ variants, selected: first, configured: "high" })
+ const third = cycleModelVariant({ variants, selected: second, configured: "high" })
+ const fourth = cycleModelVariant({ variants, selected: third, configured: "high" })
+
+ expect(first).toBe("low")
+ expect(second).toBe("high")
+ expect(third).toBe("xhigh")
+ expect(fourth).toBeUndefined()
+ })
+})