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
2 changes: 1 addition & 1 deletion packages/app/src/context/local.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { createStore } from "solid-js/store"
import { useModels } from "@/context/models"
import { useProviders } from "@/hooks/use-providers"
import { Persist, persisted } from "@/utils/persist"
import { cycleModelVariant, getConfiguredAgentVariant, resolveModelVariant } from "./model-variant"
import { cycleModelVariant, getConfiguredAgentVariant, resolveModelVariant } from "@opencode-ai/core/util/model-variant"
import { useSDK } from "./sdk"
import { useSync } from "./sync"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ type Model = AgentModel & {
variants?: Record<string, unknown>
}

// selected: string = user-chosen variant name
// selected: null = user explicitly chose "default" (clears any agent-configured variant)
// selected: undefined = no user choice yet (fall back to agent-configured variant)
type VariantInput = {
variants: string[]
selected: string | null | undefined
Expand Down Expand Up @@ -43,6 +46,7 @@ export function cycleModelVariant(input: VariantInput) {
if (index === input.variants.length - 1) return undefined
return input.variants[index + 1]
}
// No explicit selection: start cycling from the agent-configured variant.
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]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"
import { cycleModelVariant, getConfiguredAgentVariant, resolveModelVariant } from "./model-variant"
import { cycleModelVariant, getConfiguredAgentVariant, resolveModelVariant } from "../../src/util/model-variant"

describe("model variant", () => {
test("resolves configured agent variant when model matches", () => {
Expand Down Expand Up @@ -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: undefined })
const second = cycleModelVariant({ variants, selected: first, configured: undefined })
const third = cycleModelVariant({ variants, selected: second, configured: undefined })
const fourth = cycleModelVariant({ variants, selected: third, configured: undefined })

expect(first).toBe("low")
expect(second).toBe("high")
expect(third).toBe("xhigh")
expect(fourth).toBeUndefined()
})
})
9 changes: 7 additions & 2 deletions packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,13 @@ export function DialogModel(props: { providerID?: string }) {
function onSelect(providerID: string, modelID: string) {
local.model.set({ providerID, modelID }, { recent: true })
const list = local.model.variant.list()
const cur = local.model.variant.selected()
if (cur === "default" || (cur && list.includes(cur))) {
const selected = local.model.variant.selected()
const current = local.model.variant.current()
if (
selected === null ||
(selected && list.includes(selected)) ||
(!selected && current && list.includes(current))
) {
dialog.clear()
return
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export function DialogVariant() {
<DialogSelect<string>
options={options()}
title={"Select variant"}
current={local.model.variant.selected()}
current={local.model.variant.current() ?? "default"}
flat={true}
/>
)
Expand Down
8 changes: 7 additions & 1 deletion packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,13 @@ export function Prompt(props: PromptProps) {
if (!args.agent) local.agent.set(msg.agent)
if (msg.model) {
local.model.set(msg.model)
local.model.variant.set(msg.model.variant)
const configured = local.model.variant.configured()
if (!msg.model.variant || msg.model.variant === configured) {
local.model.variant.clear()
}
if (msg.model.variant && msg.model.variant !== configured) {
local.model.variant.set(msg.model.variant)
}
}
}
}
Expand Down
61 changes: 43 additions & 18 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 { useArgs } from "./args"
import { useSDK } from "./sdk"
import { RGBA } from "@opentui/core"
import { Filesystem } from "@/util/filesystem"
import { cycleModelVariant, getConfiguredAgentVariant, resolveModelVariant } from "@opencode-ai/core/util/model-variant"

export function parseModel(model: string) {
const [providerID, ...rest] = model.split("/")
Expand Down Expand Up @@ -118,7 +119,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
providerID: string
modelID: string
}[]
variant: Record<string, string | undefined>
variant: Record<string, string | null | undefined>
}>({
ready: false,
model: {},
Expand Down Expand Up @@ -149,7 +150,16 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
.then((x: any) => {
if (Array.isArray(x.recent)) setModelStore("recent", x.recent)
if (Array.isArray(x.favorite)) setModelStore("favorite", x.favorite)
if (typeof x.variant === "object" && x.variant !== null) setModelStore("variant", x.variant)
if (typeof x.variant === "object" && x.variant !== null) {
const variant = Object.fromEntries(
Object.entries(x.variant).map(([key, value]) => [
key,
value === "default" ? null : typeof value === "string" || value === null ? value : undefined,
]),
) as Record<string, string | null | undefined>
setModelStore("variant", variant)
if (Object.values(x.variant).includes("default")) state.pending = true
}
})
.catch(() => {})
.finally(() => {
Expand Down Expand Up @@ -334,17 +344,29 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
})
},
variant: {
configured() {
const a = agent.current()
const m = currentModel()
if (!m || !a) 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
const key = `${m.providerID}/${m.modelID}`
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(),
})
},
list() {
const m = currentModel()
Expand All @@ -358,23 +380,26 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const m = currentModel()
if (!m) return
const key = `${m.providerID}/${m.modelID}`
setModelStore("variant", key, value ?? "default")
setModelStore("variant", key, value ?? null)
save()
},
clear() {
const m = currentModel()
if (!m) return
const key = `${m.providerID}/${m.modelID}`
setModelStore("variant", key, undefined)
save()
},
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
18 changes: 18 additions & 0 deletions packages/opencode/test/agent/agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -740,3 +740,21 @@ 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" },
},
},
})
const build = await load(tmp.path, (svc) => svc.get("build"))
expect(build?.variant).toBe("high")
})

test("agent variant defaults to undefined when not set", async () => {
await using tmp = await tmpdir()
const build = await load(tmp.path, (svc) => svc.get("build"))
expect(build?.variant).toBeUndefined()
})
Loading