Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .opencode/agent/triage.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ mode: primary
hidden: true
model: opencode/minimax-m2.5
color: "#44BA81"
variant: "high"
tools:
"*": false
"github-triage": true
Expand Down
3 changes: 3 additions & 0 deletions packages/app/src/context/local.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,9 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
configured: this.configured(),
})
},
effective() {
return this.current()
},
list() {
const m = current()
if (!m) return []
Expand Down
29 changes: 21 additions & 8 deletions packages/app/src/context/model-variant.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,23 @@ import { describe, expect, test } from "bun:test"
import { cycleModelVariant, getConfiguredAgentVariant, resolveModelVariant } from "./model-variant"

describe("model variant", () => {
test("resolves configured agent variant when model matches", () => {
test("resolves configured agent variant when model supports variant", () => {
const value = getConfiguredAgentVariant({
agent: {
model: { providerID: "openai", modelID: "gpt-5.2" },
variant: "xhigh",
},
model: {
providerID: "openai",
modelID: "gpt-5.2",
providerID: "anthropic",
modelID: "claude-sonnet-4",
variants: { low: {}, high: {}, xhigh: {} },
},
})

expect(value).toBe("xhigh")
})

test("ignores configured variant when model does not match", () => {
test("ignores configured variant when model does not support it", () => {
const value = getConfiguredAgentVariant({
agent: {
model: { providerID: "openai", modelID: "gpt-5.2" },
Expand All @@ -27,7 +27,7 @@ describe("model variant", () => {
model: {
providerID: "anthropic",
modelID: "claude-sonnet-4",
variants: { low: {}, high: {}, xhigh: {} },
variants: { low: {}, high: {} },
},
})

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

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,
Expand All @@ -63,4 +63,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()
})
})
8 changes: 0 additions & 8 deletions packages/app/src/context/model-variant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,7 @@ type VariantInput = {

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
}
Expand All @@ -41,10 +38,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]
}
19 changes: 15 additions & 4 deletions packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,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
Expand Down Expand Up @@ -161,7 +162,17 @@ 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: { variant: info?.variant },
model: { variants: model?.variants },
})
if (msg.variant === configured) local.model.variant.set(undefined)
if (msg.variant !== configured) local.model.variant.set(msg.variant)
}
}
}
})
Expand Down Expand Up @@ -743,8 +754,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(() => {
Expand Down Expand Up @@ -1007,7 +1018,7 @@ export function Prompt(props: PromptProps) {
<Show when={showVariant()}>
<text fg={theme.textMuted}>·</text>
<text>
<span style={{ fg: theme.warning, bold: true }}>{local.model.variant.current()}</span>
<span style={{ fg: theme.warning, bold: true }}>{local.model.variant.effective()}</span>
</text>
</Show>
</box>
Expand Down
42 changes: 30 additions & 12 deletions packages/opencode/src/cli/cmd/tui/context/local.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { Provider } from "@/provider/provider"
import { useArgs } from "./args"
import { useSDK } from "./sdk"
import { RGBA } from "@opentui/core"
import { cycleModelVariant, getConfiguredAgentVariant, resolveModelVariant } from "./model-variant"

export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
name: "Local",
Expand Down Expand Up @@ -324,12 +325,33 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
})
},
variant: {
current() {
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: { variant: a.variant },
model: { variants: info?.variants },
})
},
selected() {
const m = currentModel()
if (!m) return undefined
const key = `${m.providerID}/${m.modelID}`
return modelStore.variant[key]
},
current() {
return resolveModelVariant({
variants: this.list(),
selected: this.selected(),
configured: this.configured(),
})
},
effective() {
return this.current()
},
list() {
const m = currentModel()
if (!m) return []
Expand All @@ -348,17 +370,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(),
}),
)
},
},
}
Expand Down
36 changes: 36 additions & 0 deletions packages/opencode/src/cli/cmd/tui/context/model-variant.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
type Agent = {
variant?: string
}

type Model = {
variants?: Record<string, unknown>
}

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.model?.variants) return undefined
if (!(input.agent.variant in input.model.variants)) return undefined
return input.agent.variant
}

export function resolveModelVariant(input: VariantInput) {
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 && 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]
}
28 changes: 28 additions & 0 deletions packages/opencode/test/agent/agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -687,3 +687,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()
},
})
})
40 changes: 40 additions & 0 deletions packages/opencode/test/cli/tui/model-variant.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
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 supports it", () => {
const value = getConfiguredAgentVariant({
agent: { variant: "high" },
model: { variants: { low: {}, high: {} } },
})

expect(value).toBe("high")
})

test("prefers selected variant over configured variant", () => {
const value = resolveModelVariant({
variants: ["low", "high", "xhigh"],
selected: "xhigh",
configured: "high",
})

expect(value).toBe("xhigh")
})

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()
})
})
Loading