Skip to content

Commit 6355ca1

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 6cd3a59 commit 6355ca1

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
@@ -206,6 +206,9 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
206206
configured: this.configured(),
207207
})
208208
},
209+
effective() {
210+
return this.current()
211+
},
209212
list() {
210213
const m = current()
211214
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
@@ -32,6 +32,7 @@ import { useToast } from "../../ui/toast"
3232
import { useKV } from "../../context/kv"
3333
import { useTextareaKeybindings } from "../textarea-keybindings"
3434
import { DialogSkill } from "../dialog-skill"
35+
import { getConfiguredAgentVariant } from "@tui/context/model-variant"
3536

3637
export type PromptProps = {
3738
sessionID?: string
@@ -161,7 +162,17 @@ export function Prompt(props: PromptProps) {
161162
if (msg.agent && isPrimaryAgent) {
162163
local.agent.set(msg.agent)
163164
if (msg.model) local.model.set(msg.model)
164-
if (msg.variant) local.model.variant.set(msg.variant)
165+
if (msg.variant) {
166+
const info = local.agent.list().find((x) => x.name === msg.agent)
167+
const provider = msg.model ? sync.data.provider.find((x) => x.id === msg.model.providerID) : undefined
168+
const model = msg.model ? provider?.models[msg.model.modelID] : undefined
169+
const configured = getConfiguredAgentVariant({
170+
agent: { variant: info?.variant },
171+
model: { variants: model?.variants },
172+
})
173+
if (msg.variant === configured) local.model.variant.set(undefined)
174+
if (msg.variant !== configured) local.model.variant.set(msg.variant)
175+
}
165176
}
166177
}
167178
})
@@ -743,8 +754,8 @@ export function Prompt(props: PromptProps) {
743754
const showVariant = createMemo(() => {
744755
const variants = local.model.variant.list()
745756
if (variants.length === 0) return false
746-
const current = local.model.variant.current()
747-
return !!current
757+
const effective = local.model.variant.effective()
758+
return !!effective
748759
})
749760

750761
const placeholderText = createMemo(() => {
@@ -1007,7 +1018,7 @@ export function Prompt(props: PromptProps) {
10071018
<Show when={showVariant()}>
10081019
<text fg={theme.textMuted}>·</text>
10091020
<text>
1010-
<span style={{ fg: theme.warning, bold: true }}>{local.model.variant.current()}</span>
1021+
<span style={{ fg: theme.warning, bold: true }}>{local.model.variant.effective()}</span>
10111022
</text>
10121023
</Show>
10131024
</box>

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

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { Provider } from "@/provider/provider"
1212
import { useArgs } from "./args"
1313
import { useSDK } from "./sdk"
1414
import { RGBA } from "@opentui/core"
15+
import { cycleModelVariant, getConfiguredAgentVariant, resolveModelVariant } from "./model-variant"
1516

1617
export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
1718
name: "Local",
@@ -324,12 +325,33 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
324325
})
325326
},
326327
variant: {
327-
current() {
328+
configured() {
329+
const a = agent.current()
330+
const m = currentModel()
331+
if (!m) return undefined
332+
const provider = sync.data.provider.find((x) => x.id === m.providerID)
333+
const info = provider?.models[m.modelID]
334+
return getConfiguredAgentVariant({
335+
agent: { variant: a.variant },
336+
model: { variants: info?.variants },
337+
})
338+
},
339+
selected() {
328340
const m = currentModel()
329341
if (!m) return undefined
330342
const key = `${m.providerID}/${m.modelID}`
331343
return modelStore.variant[key]
332344
},
345+
current() {
346+
return resolveModelVariant({
347+
variants: this.list(),
348+
selected: this.selected(),
349+
configured: this.configured(),
350+
})
351+
},
352+
effective() {
353+
return this.current()
354+
},
333355
list() {
334356
const m = currentModel()
335357
if (!m) return []
@@ -348,17 +370,13 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
348370
cycle() {
349371
const variants = this.list()
350372
if (variants.length === 0) return
351-
const current = this.current()
352-
if (!current) {
353-
this.set(variants[0])
354-
return
355-
}
356-
const index = variants.indexOf(current)
357-
if (index === -1 || index === variants.length - 1) {
358-
this.set(undefined)
359-
return
360-
}
361-
this.set(variants[index + 1])
373+
this.set(
374+
cycleModelVariant({
375+
variants,
376+
selected: this.selected(),
377+
configured: this.configured(),
378+
}),
379+
)
362380
},
363381
},
364382
}
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)