Skip to content

Commit 0f6f922

Browse files
feat: add agent default variant handling in TUI and desktop
- Resolve variants via selected/configured/current helper flow in both clients - Allow agent-configured variants across models when the active model supports that variant key - Update TUI hydration to avoid promoting configured defaults into explicit overrides - Add and adjust variant tests for app, TUI, and agent config - Add a triage agent variant example Closes #7138
1 parent a965a06 commit 0f6f922

9 files changed

Lines changed: 174 additions & 32 deletions

File tree

.opencode/agent/triage.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ mode: primary
33
hidden: true
44
model: opencode/minimax-m2.5
55
color: "#44BA81"
6+
variant: "high"
67
tools:
78
"*": false
89
"github-triage": true

packages/app/src/context/local.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,9 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
216216
configured: this.configured(),
217217
})
218218
},
219+
effective() {
220+
return this.current()
221+
},
219222
list() {
220223
const m = current()
221224
if (!m) return []

packages/app/src/context/model-variant.test.ts

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,23 @@ import { describe, expect, test } from "bun:test"
22
import { cycleModelVariant, getConfiguredAgentVariant, resolveModelVariant } from "./model-variant"
33

44
describe("model variant", () => {
5-
test("resolves configured agent variant when model matches", () => {
5+
test("resolves configured agent variant when model supports variant", () => {
66
const value = getConfiguredAgentVariant({
77
agent: {
88
model: { providerID: "openai", modelID: "gpt-5.2" },
99
variant: "xhigh",
1010
},
1111
model: {
12-
providerID: "openai",
13-
modelID: "gpt-5.2",
12+
providerID: "anthropic",
13+
modelID: "claude-sonnet-4",
1414
variants: { low: {}, high: {}, xhigh: {} },
1515
},
1616
})
1717

1818
expect(value).toBe("xhigh")
1919
})
2020

21-
test("ignores configured variant when model does not match", () => {
21+
test("ignores configured variant when model does not support it", () => {
2222
const value = getConfiguredAgentVariant({
2323
agent: {
2424
model: { providerID: "openai", modelID: "gpt-5.2" },
@@ -27,7 +27,7 @@ describe("model variant", () => {
2727
model: {
2828
providerID: "anthropic",
2929
modelID: "claude-sonnet-4",
30-
variants: { low: {}, high: {}, xhigh: {} },
30+
variants: { low: {}, high: {} },
3131
},
3232
})
3333

@@ -44,17 +44,17 @@ describe("model variant", () => {
4444
expect(value).toBe("high")
4545
})
4646

47-
test("cycles from configured variant to next", () => {
47+
test("starts cycling from first variant when no explicit selection", () => {
4848
const value = cycleModelVariant({
4949
variants: ["low", "high", "xhigh"],
5050
selected: undefined,
5151
configured: "high",
5252
})
5353

54-
expect(value).toBe("xhigh")
54+
expect(value).toBe("low")
5555
})
5656

57-
test("wraps from configured last variant to first", () => {
57+
test("starts from first even when configured is last", () => {
5858
const value = cycleModelVariant({
5959
variants: ["low", "high", "xhigh"],
6060
selected: undefined,
@@ -63,4 +63,17 @@ describe("model variant", () => {
6363

6464
expect(value).toBe("low")
6565
})
66+
67+
test("cycles through all variants from explicit selection", () => {
68+
const variants = ["low", "high", "xhigh"]
69+
const first = cycleModelVariant({ variants, selected: undefined, configured: "high" })
70+
const second = cycleModelVariant({ variants, selected: first, configured: "high" })
71+
const third = cycleModelVariant({ variants, selected: second, configured: "high" })
72+
const fourth = cycleModelVariant({ variants, selected: third, configured: "high" })
73+
74+
expect(first).toBe("low")
75+
expect(second).toBe("high")
76+
expect(third).toBe("xhigh")
77+
expect(fourth).toBeUndefined()
78+
})
6679
})

packages/app/src/context/model-variant.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,7 @@ type VariantInput = {
2020

2121
export function getConfiguredAgentVariant(input: { agent: Agent | undefined; model: Model | undefined }) {
2222
if (!input.agent?.variant) return undefined
23-
if (!input.agent.model) return undefined
2423
if (!input.model?.variants) return undefined
25-
if (input.agent.model.providerID !== input.model.providerID) return undefined
26-
if (input.agent.model.modelID !== input.model.modelID) return undefined
2724
if (!(input.agent.variant in input.model.variants)) return undefined
2825
return input.agent.variant
2926
}
@@ -41,10 +38,5 @@ export function cycleModelVariant(input: VariantInput) {
4138
if (index === input.variants.length - 1) return undefined
4239
return input.variants[index + 1]
4340
}
44-
if (input.configured && input.variants.includes(input.configured)) {
45-
const index = input.variants.indexOf(input.configured)
46-
if (index === input.variants.length - 1) return input.variants[0]
47-
return input.variants[index + 1]
48-
}
4941
return input.variants[0]
5042
}

packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import { useToast } from "../../ui/toast"
3434
import { useKV } from "../../context/kv"
3535
import { useTextareaKeybindings } from "../textarea-keybindings"
3636
import { DialogSkill } from "../dialog-skill"
37+
import { getConfiguredAgentVariant } from "@tui/context/model-variant"
3738

3839
export type PromptProps = {
3940
sessionID?: string
@@ -163,7 +164,17 @@ export function Prompt(props: PromptProps) {
163164
if (msg.agent && isPrimaryAgent) {
164165
local.agent.set(msg.agent)
165166
if (msg.model) local.model.set(msg.model)
166-
if (msg.variant) local.model.variant.set(msg.variant)
167+
if (msg.variant) {
168+
const info = local.agent.list().find((x) => x.name === msg.agent)
169+
const provider = msg.model ? sync.data.provider.find((x) => x.id === msg.model.providerID) : undefined
170+
const model = msg.model ? provider?.models[msg.model.modelID] : undefined
171+
const configured = getConfiguredAgentVariant({
172+
agent: { variant: info?.variant },
173+
model: { variants: model?.variants },
174+
})
175+
if (msg.variant === configured) local.model.variant.set(undefined)
176+
if (msg.variant !== configured) local.model.variant.set(msg.variant)
177+
}
167178
}
168179
}
169180
})
@@ -745,8 +756,8 @@ export function Prompt(props: PromptProps) {
745756
const showVariant = createMemo(() => {
746757
const variants = local.model.variant.list()
747758
if (variants.length === 0) return false
748-
const current = local.model.variant.current()
749-
return !!current
759+
const effective = local.model.variant.effective()
760+
return !!effective
750761
})
751762

752763
const placeholderText = createMemo(() => {
@@ -1009,7 +1020,7 @@ export function Prompt(props: PromptProps) {
10091020
<Show when={showVariant()}>
10101021
<text fg={theme.textMuted}>·</text>
10111022
<text>
1012-
<span style={{ fg: theme.warning, bold: true }}>{local.model.variant.current()}</span>
1023+
<span style={{ fg: theme.warning, bold: true }}>{local.model.variant.effective()}</span>
10131024
</text>
10141025
</Show>
10151026
</box>

packages/opencode/src/cli/cmd/tui/context/local.tsx

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { useArgs } from "./args"
1313
import { useSDK } from "./sdk"
1414
import { RGBA } from "@opentui/core"
1515
import { Filesystem } from "@/util/filesystem"
16+
import { cycleModelVariant, getConfiguredAgentVariant, resolveModelVariant } from "./model-variant"
1617

1718
export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
1819
name: "Local",
@@ -321,12 +322,33 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
321322
})
322323
},
323324
variant: {
324-
current() {
325+
configured() {
326+
const a = agent.current()
327+
const m = currentModel()
328+
if (!m) return undefined
329+
const provider = sync.data.provider.find((x) => x.id === m.providerID)
330+
const info = provider?.models[m.modelID]
331+
return getConfiguredAgentVariant({
332+
agent: { variant: a.variant },
333+
model: { variants: info?.variants },
334+
})
335+
},
336+
selected() {
325337
const m = currentModel()
326338
if (!m) return undefined
327339
const key = `${m.providerID}/${m.modelID}`
328340
return modelStore.variant[key]
329341
},
342+
current() {
343+
return resolveModelVariant({
344+
variants: this.list(),
345+
selected: this.selected(),
346+
configured: this.configured(),
347+
})
348+
},
349+
effective() {
350+
return this.current()
351+
},
330352
list() {
331353
const m = currentModel()
332354
if (!m) return []
@@ -345,17 +367,13 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
345367
cycle() {
346368
const variants = this.list()
347369
if (variants.length === 0) return
348-
const current = this.current()
349-
if (!current) {
350-
this.set(variants[0])
351-
return
352-
}
353-
const index = variants.indexOf(current)
354-
if (index === -1 || index === variants.length - 1) {
355-
this.set(undefined)
356-
return
357-
}
358-
this.set(variants[index + 1])
370+
this.set(
371+
cycleModelVariant({
372+
variants,
373+
selected: this.selected(),
374+
configured: this.configured(),
375+
}),
376+
)
359377
},
360378
},
361379
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
type Agent = {
2+
variant?: string
3+
}
4+
5+
type Model = {
6+
variants?: Record<string, unknown>
7+
}
8+
9+
type VariantInput = {
10+
variants: string[]
11+
selected: string | undefined
12+
configured: string | undefined
13+
}
14+
15+
export function getConfiguredAgentVariant(input: { agent: Agent | undefined; model: Model | undefined }) {
16+
if (!input.agent?.variant) return undefined
17+
if (!input.model?.variants) return undefined
18+
if (!(input.agent.variant in input.model.variants)) return undefined
19+
return input.agent.variant
20+
}
21+
22+
export function resolveModelVariant(input: VariantInput) {
23+
if (input.selected && input.variants.includes(input.selected)) return input.selected
24+
if (input.configured && input.variants.includes(input.configured)) return input.configured
25+
return undefined
26+
}
27+
28+
export function cycleModelVariant(input: VariantInput) {
29+
if (input.variants.length === 0) return undefined
30+
if (input.selected && input.variants.includes(input.selected)) {
31+
const index = input.variants.indexOf(input.selected)
32+
if (index === input.variants.length - 1) return undefined
33+
return input.variants[index + 1]
34+
}
35+
return input.variants[0]
36+
}

packages/opencode/test/agent/agent.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -687,3 +687,31 @@ test("defaultAgent throws when all primary agents are disabled", async () => {
687687
},
688688
})
689689
})
690+
691+
test("agent variant can be set from config", async () => {
692+
await using tmp = await tmpdir({
693+
config: {
694+
agent: {
695+
build: { variant: "high" },
696+
},
697+
},
698+
})
699+
await Instance.provide({
700+
directory: tmp.path,
701+
fn: async () => {
702+
const build = await Agent.get("build")
703+
expect(build?.variant).toBe("high")
704+
},
705+
})
706+
})
707+
708+
test("agent variant defaults to undefined when not set", async () => {
709+
await using tmp = await tmpdir()
710+
await Instance.provide({
711+
directory: tmp.path,
712+
fn: async () => {
713+
const build = await Agent.get("build")
714+
expect(build?.variant).toBeUndefined()
715+
},
716+
})
717+
})
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { describe, expect, test } from "bun:test"
2+
import {
3+
cycleModelVariant,
4+
getConfiguredAgentVariant,
5+
resolveModelVariant,
6+
} from "../../../src/cli/cmd/tui/context/model-variant"
7+
8+
describe("tui model variant", () => {
9+
test("resolves configured variant when active model supports it", () => {
10+
const value = getConfiguredAgentVariant({
11+
agent: { variant: "high" },
12+
model: { variants: { low: {}, high: {} } },
13+
})
14+
15+
expect(value).toBe("high")
16+
})
17+
18+
test("prefers selected variant over configured variant", () => {
19+
const value = resolveModelVariant({
20+
variants: ["low", "high", "xhigh"],
21+
selected: "xhigh",
22+
configured: "high",
23+
})
24+
25+
expect(value).toBe("xhigh")
26+
})
27+
28+
test("cycles through all variants from explicit selection", () => {
29+
const variants = ["low", "high", "xhigh"]
30+
const first = cycleModelVariant({ variants, selected: undefined, configured: "high" })
31+
const second = cycleModelVariant({ variants, selected: first, configured: "high" })
32+
const third = cycleModelVariant({ variants, selected: second, configured: "high" })
33+
const fourth = cycleModelVariant({ variants, selected: third, configured: "high" })
34+
35+
expect(first).toBe("low")
36+
expect(second).toBe("high")
37+
expect(third).toBe("xhigh")
38+
expect(fourth).toBeUndefined()
39+
})
40+
})

0 commit comments

Comments
 (0)