Skip to content

Commit 2b8853c

Browse files
committed
feat(config): add model variant support
Allow optional model variant config for agents and categories. Propagate category variants into task model payloads so category-driven runs inherit provider-specific variants. Closes: #647
1 parent f9fce50 commit 2b8853c

File tree

18 files changed

+452
-12
lines changed

18 files changed

+452
-12
lines changed

assets/oh-my-opencode.schema.json

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,9 @@
102102
"model": {
103103
"type": "string"
104104
},
105+
"variant": {
106+
"type": "string"
107+
},
105108
"category": {
106109
"type": "string"
107110
},
@@ -225,6 +228,9 @@
225228
"model": {
226229
"type": "string"
227230
},
231+
"variant": {
232+
"type": "string"
233+
},
228234
"category": {
229235
"type": "string"
230236
},
@@ -348,6 +354,9 @@
348354
"model": {
349355
"type": "string"
350356
},
357+
"variant": {
358+
"type": "string"
359+
},
351360
"category": {
352361
"type": "string"
353362
},
@@ -471,6 +480,9 @@
471480
"model": {
472481
"type": "string"
473482
},
483+
"variant": {
484+
"type": "string"
485+
},
474486
"category": {
475487
"type": "string"
476488
},
@@ -594,6 +606,9 @@
594606
"model": {
595607
"type": "string"
596608
},
609+
"variant": {
610+
"type": "string"
611+
},
597612
"category": {
598613
"type": "string"
599614
},
@@ -717,6 +732,9 @@
717732
"model": {
718733
"type": "string"
719734
},
735+
"variant": {
736+
"type": "string"
737+
},
720738
"category": {
721739
"type": "string"
722740
},
@@ -840,6 +858,9 @@
840858
"model": {
841859
"type": "string"
842860
},
861+
"variant": {
862+
"type": "string"
863+
},
843864
"category": {
844865
"type": "string"
845866
},
@@ -963,6 +984,9 @@
963984
"model": {
964985
"type": "string"
965986
},
987+
"variant": {
988+
"type": "string"
989+
},
966990
"category": {
967991
"type": "string"
968992
},
@@ -1086,6 +1110,9 @@
10861110
"model": {
10871111
"type": "string"
10881112
},
1113+
"variant": {
1114+
"type": "string"
1115+
},
10891116
"category": {
10901117
"type": "string"
10911118
},
@@ -1209,6 +1236,9 @@
12091236
"model": {
12101237
"type": "string"
12111238
},
1239+
"variant": {
1240+
"type": "string"
1241+
},
12121242
"category": {
12131243
"type": "string"
12141244
},
@@ -1332,6 +1362,9 @@
13321362
"model": {
13331363
"type": "string"
13341364
},
1365+
"variant": {
1366+
"type": "string"
1367+
},
13351368
"category": {
13361369
"type": "string"
13371370
},
@@ -1455,6 +1488,9 @@
14551488
"model": {
14561489
"type": "string"
14571490
},
1491+
"variant": {
1492+
"type": "string"
1493+
},
14581494
"category": {
14591495
"type": "string"
14601496
},
@@ -1578,6 +1614,9 @@
15781614
"model": {
15791615
"type": "string"
15801616
},
1617+
"variant": {
1618+
"type": "string"
1619+
},
15811620
"category": {
15821621
"type": "string"
15831622
},
@@ -1701,6 +1740,9 @@
17011740
"model": {
17021741
"type": "string"
17031742
},
1743+
"variant": {
1744+
"type": "string"
1745+
},
17041746
"category": {
17051747
"type": "string"
17061748
},
@@ -1824,6 +1866,9 @@
18241866
"model": {
18251867
"type": "string"
18261868
},
1869+
"variant": {
1870+
"type": "string"
1871+
},
18271872
"category": {
18281873
"type": "string"
18291874
},
@@ -1954,6 +1999,9 @@
19541999
"model": {
19552000
"type": "string"
19562001
},
2002+
"variant": {
2003+
"type": "string"
2004+
},
19572005
"temperature": {
19582006
"type": "number",
19592007
"minimum": 0,

src/agents/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ export type AgentName = BuiltinAgentName
7676

7777
export type AgentOverrideConfig = Partial<AgentConfig> & {
7878
prompt_append?: string
79+
variant?: string
7980
}
8081

8182
export type AgentOverrides = Partial<Record<OverridableAgentName, AgentOverrideConfig>>

src/agents/utils.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,31 @@ describe("buildAgent with category and skills", () => {
127127
expect(agent.temperature).toBe(0.7)
128128
})
129129

130+
test("agent with category inherits variant", () => {
131+
// #given
132+
const source = {
133+
"test-agent": () =>
134+
({
135+
description: "Test agent",
136+
category: "custom-category",
137+
}) as AgentConfig,
138+
}
139+
140+
const categories = {
141+
"custom-category": {
142+
model: "openai/gpt-5.2",
143+
variant: "xhigh",
144+
},
145+
}
146+
147+
// #when
148+
const agent = buildAgent(source["test-agent"], undefined, categories)
149+
150+
// #then
151+
expect(agent.model).toBe("openai/gpt-5.2")
152+
expect(agent.variant).toBe("xhigh")
153+
})
154+
130155
test("agent with skills has content prepended to prompt", () => {
131156
// #given
132157
const source = {

src/agents/utils.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { AgentConfig } from "@opencode-ai/sdk"
22
import type { BuiltinAgentName, AgentOverrideConfig, AgentOverrides, AgentFactory, AgentPromptMetadata } from "./types"
3+
import type { CategoriesConfig, CategoryConfig } from "../config/schema"
34
import { createSisyphusAgent } from "./sisyphus"
45
import { createOracleAgent, ORACLE_PROMPT_METADATA } from "./oracle"
56
import { createLibrarianAgent, LIBRARIAN_PROMPT_METADATA } from "./librarian"
@@ -47,19 +48,29 @@ function isFactory(source: AgentSource): source is AgentFactory {
4748
return typeof source === "function"
4849
}
4950

50-
export function buildAgent(source: AgentSource, model?: string): AgentConfig {
51+
export function buildAgent(
52+
source: AgentSource,
53+
model?: string,
54+
categories?: CategoriesConfig
55+
): AgentConfig {
5156
const base = isFactory(source) ? source(model) : source
57+
const categoryConfigs: Record<string, CategoryConfig> = categories
58+
? { ...DEFAULT_CATEGORIES, ...categories }
59+
: DEFAULT_CATEGORIES
5260

53-
const agentWithCategory = base as AgentConfig & { category?: string; skills?: string[] }
61+
const agentWithCategory = base as AgentConfig & { category?: string; skills?: string[]; variant?: string }
5462
if (agentWithCategory.category) {
55-
const categoryConfig = DEFAULT_CATEGORIES[agentWithCategory.category]
63+
const categoryConfig = categoryConfigs[agentWithCategory.category]
5664
if (categoryConfig) {
5765
if (!base.model) {
5866
base.model = categoryConfig.model
5967
}
6068
if (base.temperature === undefined && categoryConfig.temperature !== undefined) {
6169
base.temperature = categoryConfig.temperature
6270
}
71+
if (base.variant === undefined && categoryConfig.variant !== undefined) {
72+
base.variant = categoryConfig.variant
73+
}
6374
}
6475
}
6576

@@ -118,11 +129,16 @@ export function createBuiltinAgents(
118129
disabledAgents: BuiltinAgentName[] = [],
119130
agentOverrides: AgentOverrides = {},
120131
directory?: string,
121-
systemDefaultModel?: string
132+
systemDefaultModel?: string,
133+
categories?: CategoriesConfig
122134
): Record<string, AgentConfig> {
123135
const result: Record<string, AgentConfig> = {}
124136
const availableAgents: AvailableAgent[] = []
125137

138+
const mergedCategories = categories
139+
? { ...DEFAULT_CATEGORIES, ...categories }
140+
: DEFAULT_CATEGORIES
141+
126142
for (const [name, source] of Object.entries(agentSources)) {
127143
const agentName = name as BuiltinAgentName
128144

@@ -133,7 +149,7 @@ export function createBuiltinAgents(
133149
const override = agentOverrides[agentName]
134150
const model = override?.model
135151

136-
let config = buildAgent(source, model)
152+
let config = buildAgent(source, model, mergedCategories)
137153

138154
if (agentName === "librarian" && directory && config.prompt) {
139155
const envContext = createEnvContext()

src/config/schema.test.ts

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, expect, test } from "bun:test"
2-
import { AgentOverrideConfigSchema, BuiltinCategoryNameSchema, OhMyOpenCodeConfigSchema } from "./schema"
2+
import { AgentOverrideConfigSchema, BuiltinCategoryNameSchema, CategoryConfigSchema, OhMyOpenCodeConfigSchema } from "./schema"
33

44
describe("disabled_mcps schema", () => {
55
test("should accept built-in MCP names", () => {
@@ -174,6 +174,33 @@ describe("AgentOverrideConfigSchema", () => {
174174
})
175175
})
176176

177+
describe("variant field", () => {
178+
test("accepts variant as optional string", () => {
179+
// #given
180+
const config = { variant: "high" }
181+
182+
// #when
183+
const result = AgentOverrideConfigSchema.safeParse(config)
184+
185+
// #then
186+
expect(result.success).toBe(true)
187+
if (result.success) {
188+
expect(result.data.variant).toBe("high")
189+
}
190+
})
191+
192+
test("rejects non-string variant", () => {
193+
// #given
194+
const config = { variant: 123 }
195+
196+
// #when
197+
const result = AgentOverrideConfigSchema.safeParse(config)
198+
199+
// #then
200+
expect(result.success).toBe(false)
201+
})
202+
})
203+
177204
describe("skills field", () => {
178205
test("accepts skills as optional string array", () => {
179206
// #given
@@ -303,6 +330,33 @@ describe("AgentOverrideConfigSchema", () => {
303330
})
304331
})
305332

333+
describe("CategoryConfigSchema", () => {
334+
test("accepts variant as optional string", () => {
335+
// #given
336+
const config = { model: "openai/gpt-5.2", variant: "xhigh" }
337+
338+
// #when
339+
const result = CategoryConfigSchema.safeParse(config)
340+
341+
// #then
342+
expect(result.success).toBe(true)
343+
if (result.success) {
344+
expect(result.data.variant).toBe("xhigh")
345+
}
346+
})
347+
348+
test("rejects non-string variant", () => {
349+
// #given
350+
const config = { model: "openai/gpt-5.2", variant: 123 }
351+
352+
// #when
353+
const result = CategoryConfigSchema.safeParse(config)
354+
355+
// #then
356+
expect(result.success).toBe(false)
357+
})
358+
})
359+
306360
describe("BuiltinCategoryNameSchema", () => {
307361
test("accepts all builtin category names", () => {
308362
// #given

src/config/schema.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ export const BuiltinCommandNameSchema = z.enum([
9797
export const AgentOverrideConfigSchema = z.object({
9898
/** @deprecated Use `category` instead. Model is inherited from category defaults. */
9999
model: z.string().optional(),
100+
variant: z.string().optional(),
100101
/** Category name to inherit model and other settings from CategoryConfig */
101102
category: z.string().optional(),
102103
/** Skill names to inject into agent prompt */
@@ -153,6 +154,7 @@ export const SisyphusAgentConfigSchema = z.object({
153154

154155
export const CategoryConfigSchema = z.object({
155156
model: z.string(),
157+
variant: z.string().optional(),
156158
temperature: z.number().min(0).max(2).optional(),
157159
top_p: z.number().min(0).max(1).optional(),
158160
maxTokens: z.number().optional(),

src/features/background-agent/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export interface BackgroundTask {
2727
error?: string
2828
progress?: TaskProgress
2929
parentModel?: { providerID: string; modelID: string }
30-
model?: { providerID: string; modelID: string }
30+
model?: { providerID: string; modelID: string; variant?: string }
3131
/** Agent name used for concurrency tracking */
3232
concurrencyKey?: string
3333
/** Parent session's agent name for notification */
@@ -46,7 +46,7 @@ export interface LaunchInput {
4646
parentMessageID: string
4747
parentModel?: { providerID: string; modelID: string }
4848
parentAgent?: string
49-
model?: { providerID: string; modelID: string }
49+
model?: { providerID: string; modelID: string; variant?: string }
5050
skills?: string[]
5151
skillContent?: string
5252
}

0 commit comments

Comments
 (0)