Skip to content

Commit 99a76ac

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 a8b2882 commit 99a76ac

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
@@ -321,6 +321,9 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
321321
configured: this.configured(),
322322
})
323323
},
324+
effective() {
325+
return this.current()
326+
},
324327
list() {
325328
const item = current()
326329
if (!item?.variants) 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

@@ -54,17 +54,17 @@ describe("model variant", () => {
5454
expect(value).toBeUndefined()
5555
})
5656

57-
test("cycles from configured variant to next", () => {
57+
test("starts cycling from first variant when no explicit selection", () => {
5858
const value = cycleModelVariant({
5959
variants: ["low", "high", "xhigh"],
6060
selected: undefined,
6161
configured: "high",
6262
})
6363

64-
expect(value).toBe("xhigh")
64+
expect(value).toBe("low")
6565
})
6666

67-
test("wraps from configured last variant to first", () => {
67+
test("starts from first even when configured is last", () => {
6868
const value = cycleModelVariant({
6969
variants: ["low", "high", "xhigh"],
7070
selected: undefined,
@@ -83,4 +83,17 @@ describe("model variant", () => {
8383

8484
expect(value).toBe("low")
8585
})
86+
87+
test("cycles through all variants from explicit selection", () => {
88+
const variants = ["low", "high", "xhigh"]
89+
const first = cycleModelVariant({ variants, selected: undefined, configured: "high" })
90+
const second = cycleModelVariant({ variants, selected: first, configured: "high" })
91+
const third = cycleModelVariant({ variants, selected: second, configured: "high" })
92+
const fourth = cycleModelVariant({ variants, selected: third, configured: "high" })
93+
94+
expect(first).toBe("low")
95+
expect(second).toBe("high")
96+
expect(third).toBe("xhigh")
97+
expect(fourth).toBeUndefined()
98+
})
8699
})

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
}
@@ -43,10 +40,5 @@ export function cycleModelVariant(input: VariantInput) {
4340
if (index === input.variants.length - 1) return undefined
4441
return input.variants[index + 1]
4542
}
46-
if (input.configured && input.variants.includes(input.configured)) {
47-
const index = input.variants.indexOf(input.configured)
48-
if (index === input.variants.length - 1) return input.variants[0]
49-
return input.variants[index + 1]
50-
}
5143
return input.variants[0]
5244
}

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

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

3940
export type PromptProps = {
4041
sessionID?: string
@@ -165,7 +166,17 @@ export function Prompt(props: PromptProps) {
165166
if (msg.agent && isPrimaryAgent) {
166167
local.agent.set(msg.agent)
167168
if (msg.model) local.model.set(msg.model)
168-
if (msg.variant) local.model.variant.set(msg.variant)
169+
if (msg.variant) {
170+
const info = local.agent.list().find((x) => x.name === msg.agent)
171+
const provider = msg.model ? sync.data.provider.find((x) => x.id === msg.model.providerID) : undefined
172+
const model = msg.model ? provider?.models[msg.model.modelID] : undefined
173+
const configured = getConfiguredAgentVariant({
174+
agent: { variant: info?.variant },
175+
model: { variants: model?.variants },
176+
})
177+
if (msg.variant === configured) local.model.variant.set(undefined)
178+
if (msg.variant !== configured) local.model.variant.set(msg.variant)
179+
}
169180
}
170181
}
171182
})
@@ -759,8 +770,8 @@ export function Prompt(props: PromptProps) {
759770
const showVariant = createMemo(() => {
760771
const variants = local.model.variant.list()
761772
if (variants.length === 0) return false
762-
const current = local.model.variant.current()
763-
return !!current
773+
const effective = local.model.variant.effective()
774+
return !!effective
764775
})
765776

766777
const placeholderText = createMemo(() => {
@@ -1023,7 +1034,7 @@ export function Prompt(props: PromptProps) {
10231034
<Show when={showVariant()}>
10241035
<text fg={theme.textMuted}>·</text>
10251036
<text>
1026-
<span style={{ fg: theme.warning, bold: true }}>{local.model.variant.current()}</span>
1037+
<span style={{ fg: theme.warning, bold: true }}>{local.model.variant.effective()}</span>
10271038
</text>
10281039
</Show>
10291040
</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
@@ -717,3 +717,31 @@ test("defaultAgent throws when all primary agents are disabled", async () => {
717717
},
718718
})
719719
})
720+
721+
test("agent variant can be set from config", async () => {
722+
await using tmp = await tmpdir({
723+
config: {
724+
agent: {
725+
build: { variant: "high" },
726+
},
727+
},
728+
})
729+
await Instance.provide({
730+
directory: tmp.path,
731+
fn: async () => {
732+
const build = await Agent.get("build")
733+
expect(build?.variant).toBe("high")
734+
},
735+
})
736+
})
737+
738+
test("agent variant defaults to undefined when not set", async () => {
739+
await using tmp = await tmpdir()
740+
await Instance.provide({
741+
directory: tmp.path,
742+
fn: async () => {
743+
const build = await Agent.get("build")
744+
expect(build?.variant).toBeUndefined()
745+
},
746+
})
747+
})
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)