diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index 1d90a4c3656b..72e7f8985da1 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -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(), @@ -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 diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 7969e3079574..98970ba392dc 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -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(), @@ -624,6 +628,7 @@ export namespace Config { const knownKeys = new Set([ "name", "model", + "variant", "prompt", "description", "temperature", diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 94eabdef7f48..ba77cd7ca3e7 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -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", @@ -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)) diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index 1752e22e01f6..8611d8296976 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -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) => { diff --git a/packages/opencode/test/session/prompt-variant.test.ts b/packages/opencode/test/session/prompt-variant.test.ts new file mode 100644 index 000000000000..16e8a22444c7 --- /dev/null +++ b/packages/opencode/test/session/prompt-variant.test.ts @@ -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) + }, + }) + }) +})