Skip to content
Merged
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: 2 additions & 0 deletions packages/opencode/src/agent/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export namespace Agent {
providerID: z.string(),
})
.optional(),
variant: z.string().optional(),
prompt: z.string().optional(),
options: z.record(z.string(), z.any()),
steps: z.number().int().positive().optional(),
Expand Down Expand Up @@ -214,6 +215,7 @@ export namespace Agent {
native: false,
}
if (value.model) item.model = Provider.parseModel(value.model)
item.variant = value.variant ?? item.variant
item.prompt = value.prompt ?? item.prompt
item.description = value.description ?? item.description
item.temperature = value.temperature ?? item.temperature
Expand Down
5 changes: 5 additions & 0 deletions packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,10 @@ export namespace Config {
export const Agent = z
.object({
model: z.string().optional(),
variant: z
.string()
.optional()
.describe("Default model variant for this agent (applies only when using the agent's configured model)."),
temperature: z.number().optional(),
top_p: z.number().optional(),
prompt: z.string().optional(),
Expand Down Expand Up @@ -624,6 +628,7 @@ export namespace Config {
const knownKeys = new Set([
"name",
"model",
"variant",
"prompt",
"description",
"temperature",
Expand Down
15 changes: 13 additions & 2 deletions packages/opencode/src/session/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -827,6 +827,17 @@ export namespace SessionPrompt {

async function createUserMessage(input: PromptInput) {
const agent = await Agent.get(input.agent ?? (await Agent.defaultAgent()))

const model = input.model ?? agent.model ?? (await lastModel(input.sessionID))
const variant =
input.variant ??
(agent.variant &&
agent.model &&
model.providerID === agent.model.providerID &&
model.modelID === agent.model.modelID
? agent.variant
: undefined)

const info: MessageV2.Info = {
id: input.messageID ?? Identifier.ascending("message"),
role: "user",
Expand All @@ -836,9 +847,9 @@ export namespace SessionPrompt {
},
tools: input.tools,
agent: agent.name,
model: input.model ?? agent.model ?? (await lastModel(input.sessionID)),
model,
system: input.system,
variant: input.variant,
variant,
}
using _ = defer(() => InstructionPrompt.clear(info.id))

Expand Down
31 changes: 31 additions & 0 deletions packages/opencode/test/config/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,37 @@ test("handles agent configuration", async () => {
})
})

test("treats agent variant as model-scoped setting (not provider option)", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await writeConfig(dir, {
$schema: "https://opencode.ai/config.json",
agent: {
test_agent: {
model: "openai/gpt-5.2",
variant: "xhigh",
max_tokens: 123,
},
},
})
},
})

await Instance.provide({
directory: tmp.path,
fn: async () => {
const config = await Config.get()
const agent = config.agent?.["test_agent"]

expect(agent?.variant).toBe("xhigh")
expect(agent?.options).toMatchObject({
max_tokens: 123,
})
expect(agent?.options).not.toHaveProperty("variant")
},
})
})

test("handles command configuration", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
Expand Down
60 changes: 60 additions & 0 deletions packages/opencode/test/session/prompt-variant.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { describe, expect, test } from "bun:test"
import { Instance } from "../../src/project/instance"
import { Session } from "../../src/session"
import { SessionPrompt } from "../../src/session/prompt"
import { tmpdir } from "../fixture/fixture"

describe("session.prompt agent variant", () => {
test("applies agent variant only when using agent model", async () => {
await using tmp = await tmpdir({
git: true,
config: {
agent: {
build: {
model: "openai/gpt-5.2",
variant: "xhigh",
},
},
},
})

await Instance.provide({
directory: tmp.path,
fn: async () => {
const session = await Session.create({})

const other = await SessionPrompt.prompt({
sessionID: session.id,
agent: "build",
model: { providerID: "opencode", modelID: "kimi-k2.5-free" },
noReply: true,
parts: [{ type: "text", text: "hello" }],
})
if (other.info.role !== "user") throw new Error("expected user message")
expect(other.info.variant).toBeUndefined()

const match = await SessionPrompt.prompt({
sessionID: session.id,
agent: "build",
noReply: true,
parts: [{ type: "text", text: "hello again" }],
})
if (match.info.role !== "user") throw new Error("expected user message")
expect(match.info.model).toEqual({ providerID: "openai", modelID: "gpt-5.2" })
expect(match.info.variant).toBe("xhigh")

const override = await SessionPrompt.prompt({
sessionID: session.id,
agent: "build",
noReply: true,
variant: "high",
parts: [{ type: "text", text: "hello third" }],
})
if (override.info.role !== "user") throw new Error("expected user message")
expect(override.info.variant).toBe("high")

await Session.remove(session.id)
},
})
})
})
Loading