From d92432d1a81c50a2772d4745a9c7adc32c21b89d Mon Sep 17 00:00:00 2001 From: CasualDeveloper <10153929+CasualDeveloper@users.noreply.github.com> Date: Thu, 8 Jan 2026 13:32:49 +0800 Subject: [PATCH] feat: add agent-level default variant support - Add variant field to agent config schema - Resolve variant with priority: user input > agent config > none - Display effective variant in TUI and desktop prompt bar - Subagents use their own configured variant (not inherited) - Add variant examples to triage and duplicate-pr agents - Add unit tests for agent variant configuration Closes #7138 --- .opencode/agent/triage.md | 1 + packages/app/src/components/prompt-input.tsx | 4 +-- packages/app/src/context/local.tsx | 9 ++++-- packages/opencode/src/agent/agent.ts | 2 ++ .../cli/cmd/tui/component/prompt/index.tsx | 6 ++-- .../src/cli/cmd/tui/context/local.tsx | 9 ++++-- packages/opencode/src/config/config.ts | 2 ++ packages/opencode/src/session/llm.ts | 9 ++++-- packages/opencode/src/session/prompt.ts | 2 +- packages/opencode/test/agent/agent.test.ts | 28 +++++++++++++++++++ packages/sdk/js/src/v2/gen/types.gen.ts | 5 ++++ packages/sdk/openapi.json | 7 +++++ 12 files changed, 70 insertions(+), 14 deletions(-) diff --git a/.opencode/agent/triage.md b/.opencode/agent/triage.md index 5d1147a8859..1775a4c3166 100644 --- a/.opencode/agent/triage.md +++ b/.opencode/agent/triage.md @@ -3,6 +3,7 @@ mode: primary hidden: true model: opencode/claude-haiku-4-5 color: "#44BA81" +variant: "high" tools: "*": false "github-triage": true diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index c736ef0f1ae..35523ae201e 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -1231,7 +1231,7 @@ export const PromptInput: Component = (props) => { providerID: currentModel.provider.id, } const agent = currentAgent.name - const variant = local.model.variant.current() + const variant = local.model.variant.effective() const clearInput = () => { prompt.reset() @@ -1955,7 +1955,7 @@ export const PromptInput: Component = (props) => { class="text-text-base _hidden group-hover/prompt-input:inline-block capitalize text-12-regular" onClick={() => local.model.variant.cycle()} > - {local.model.variant.current() ?? language.t("common.default")} + {local.model.variant.effective() ?? language.t("common.default")} diff --git a/packages/app/src/context/local.tsx b/packages/app/src/context/local.tsx index f51bb693092..3bc1a7f4e40 100644 --- a/packages/app/src/context/local.tsx +++ b/packages/app/src/context/local.tsx @@ -189,6 +189,9 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ if (!m) return undefined return models.variant.get({ providerID: m.provider.id, modelID: m.id }) }, + effective() { + return this.current() ?? agent.current()?.variant + }, list() { const m = current() if (!m) return [] @@ -203,12 +206,12 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ cycle() { const variants = this.list() if (variants.length === 0) return - const currentVariant = this.current() - if (!currentVariant) { + const effective = this.effective() + if (!effective) { this.set(variants[0]) return } - const index = variants.indexOf(currentVariant) + const index = variants.indexOf(effective) if (index === -1 || index === variants.length - 1) { this.set(undefined) return diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index 1d90a4c3656..e89d99cb193 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -38,6 +38,7 @@ export namespace Agent { }) .optional(), prompt: z.string().optional(), + variant: z.string().optional(), options: z.record(z.string(), z.any()), steps: z.number().int().positive().optional(), }) @@ -214,6 +215,7 @@ export namespace Agent { native: false, } if (value.model) item.model = Provider.parseModel(value.model) + if (value.variant) item.variant = value.variant item.prompt = value.prompt ?? item.prompt item.description = value.description ?? item.description item.temperature = value.temperature ?? item.temperature diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index e19c8b70982..a73ef44f1af 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -706,8 +706,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 spinnerDef = createMemo(() => { @@ -956,7 +956,7 @@ export function Prompt(props: PromptProps) { ยท - {local.model.variant.current()} + {local.model.variant.effective()} diff --git a/packages/opencode/src/cli/cmd/tui/context/local.tsx b/packages/opencode/src/cli/cmd/tui/context/local.tsx index d058ce54fb3..65ebf2b2727 100644 --- a/packages/opencode/src/cli/cmd/tui/context/local.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/local.tsx @@ -323,6 +323,9 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ const key = `${m.providerID}/${m.modelID}` return modelStore.variant[key] }, + effective() { + return this.current() ?? agent.current().variant + }, list() { const m = currentModel() if (!m) return [] @@ -341,12 +344,12 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ cycle() { const variants = this.list() if (variants.length === 0) return - const current = this.current() - if (!current) { + const effective = this.effective() + if (!effective) { this.set(variants[0]) return } - const index = variants.indexOf(current) + const index = variants.indexOf(effective) if (index === -1 || index === variants.length - 1) { this.set(undefined) return diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 020e626cba8..be6dae3ba7d 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -562,6 +562,7 @@ export namespace Config { export const Agent = z .object({ model: z.string().optional(), + variant: z.string().optional().describe("Default variant to use for this agent"), temperature: z.number().optional(), top_p: z.number().optional(), prompt: z.string().optional(), @@ -593,6 +594,7 @@ export namespace Config { const knownKeys = new Set([ "name", "model", + "variant", "prompt", "description", "temperature", diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index d651308032e..863c12c1533 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -98,8 +98,13 @@ export namespace LLM { system.push(header, rest.join("\n")) } - const variant = - !input.small && input.model.variants && input.user.variant ? input.model.variants[input.user.variant] : {} + const variant = (() => { + if (input.small) return {} + if (!input.model.variants) return {} + const selectedVariant = input.user.variant ?? input.agent.variant + if (!selectedVariant) return {} + return input.model.variants[selectedVariant] ?? {} + })() const base = input.small ? ProviderTransform.smallOptions(input.model) : ProviderTransform.options({ diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 23ca473541c..ddcc947de49 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -837,7 +837,7 @@ export namespace SessionPrompt { agent: agent.name, model: input.model ?? agent.model ?? (await lastModel(input.sessionID)), system: input.system, - variant: input.variant, + variant: input.variant ?? agent.variant, } const parts = await Promise.all( diff --git a/packages/opencode/test/agent/agent.test.ts b/packages/opencode/test/agent/agent.test.ts index 1ff303b7662..8b806ec9dac 100644 --- a/packages/opencode/test/agent/agent.test.ts +++ b/packages/opencode/test/agent/agent.test.ts @@ -636,3 +636,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() + }, + }) +}) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 2a63d721215..95c75ecb1cb 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1383,6 +1383,10 @@ export type PermissionConfig = export type AgentConfig = { model?: string + /** + * Default variant to use for this agent + */ + variant?: string temperature?: number top_p?: number prompt?: string @@ -2123,6 +2127,7 @@ export type Agent = { providerID: string } prompt?: string + variant?: string options: { [key: string]: unknown } diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index cf2f29d8589..cf3bcc3868d 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -8995,6 +8995,10 @@ "model": { "type": "string" }, + "variant": { + "description": "Default variant to use for this agent", + "type": "string" + }, "temperature": { "type": "number" }, @@ -10765,6 +10769,9 @@ "prompt": { "type": "string" }, + "variant": { + "type": "string" + }, "options": { "type": "object", "propertyNames": {