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
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import type { OhMyOpenCodeConfig } from "../config"
|
||||
import { applyAgentVariant, resolveAgentVariant } from "./agent-variant"
|
||||
|
||||
describe("resolveAgentVariant", () => {
|
||||
test("returns undefined when agent name missing", () => {
|
||||
// #given
|
||||
const config = {} as OhMyOpenCodeConfig
|
||||
|
||||
// #when
|
||||
const variant = resolveAgentVariant(config)
|
||||
|
||||
// #then
|
||||
expect(variant).toBeUndefined()
|
||||
})
|
||||
|
||||
test("returns agent override variant", () => {
|
||||
// #given
|
||||
const config = {
|
||||
agents: {
|
||||
Sisyphus: { variant: "low" },
|
||||
},
|
||||
} as OhMyOpenCodeConfig
|
||||
|
||||
// #when
|
||||
const variant = resolveAgentVariant(config, "Sisyphus")
|
||||
|
||||
// #then
|
||||
expect(variant).toBe("low")
|
||||
})
|
||||
|
||||
test("returns category variant when agent uses category", () => {
|
||||
// #given
|
||||
const config = {
|
||||
agents: {
|
||||
Sisyphus: { category: "ultrabrain" },
|
||||
},
|
||||
categories: {
|
||||
ultrabrain: { model: "openai/gpt-5.2", variant: "xhigh" },
|
||||
},
|
||||
} as OhMyOpenCodeConfig
|
||||
|
||||
// #when
|
||||
const variant = resolveAgentVariant(config, "Sisyphus")
|
||||
|
||||
// #then
|
||||
expect(variant).toBe("xhigh")
|
||||
})
|
||||
})
|
||||
|
||||
describe("applyAgentVariant", () => {
|
||||
test("sets variant when message is undefined", () => {
|
||||
// #given
|
||||
const config = {
|
||||
agents: {
|
||||
Sisyphus: { variant: "low" },
|
||||
},
|
||||
} as OhMyOpenCodeConfig
|
||||
const message: { variant?: string } = {}
|
||||
|
||||
// #when
|
||||
applyAgentVariant(config, "Sisyphus", message)
|
||||
|
||||
// #then
|
||||
expect(message.variant).toBe("low")
|
||||
})
|
||||
|
||||
test("does not override existing variant", () => {
|
||||
// #given
|
||||
const config = {
|
||||
agents: {
|
||||
Sisyphus: { variant: "low" },
|
||||
},
|
||||
} as OhMyOpenCodeConfig
|
||||
const message = { variant: "max" }
|
||||
|
||||
// #when
|
||||
applyAgentVariant(config, "Sisyphus", message)
|
||||
|
||||
// #then
|
||||
expect(message.variant).toBe("max")
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { OhMyOpenCodeConfig } from "../config"
|
||||
|
||||
export function resolveAgentVariant(
|
||||
config: OhMyOpenCodeConfig,
|
||||
agentName?: string
|
||||
): string | undefined {
|
||||
if (!agentName) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const agentOverrides = config.agents as
|
||||
| Record<string, { variant?: string; category?: string }>
|
||||
| undefined
|
||||
const agentOverride = agentOverrides?.[agentName]
|
||||
if (!agentOverride) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (agentOverride.variant) {
|
||||
return agentOverride.variant
|
||||
}
|
||||
|
||||
const categoryName = agentOverride.category
|
||||
if (!categoryName) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return config.categories?.[categoryName]?.variant
|
||||
}
|
||||
|
||||
export function applyAgentVariant(
|
||||
config: OhMyOpenCodeConfig,
|
||||
agentName: string | undefined,
|
||||
message: { variant?: string }
|
||||
): void {
|
||||
const variant = resolveAgentVariant(config, agentName)
|
||||
if (variant !== undefined && message.variant === undefined) {
|
||||
message.variant = variant
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { createFirstMessageVariantGate } from "./first-message-variant"
|
||||
|
||||
describe("createFirstMessageVariantGate", () => {
|
||||
test("marks new sessions and clears after apply", () => {
|
||||
// #given
|
||||
const gate = createFirstMessageVariantGate()
|
||||
|
||||
// #when
|
||||
gate.markSessionCreated({ id: "session-1" })
|
||||
|
||||
// #then
|
||||
expect(gate.shouldOverride("session-1")).toBe(true)
|
||||
|
||||
// #when
|
||||
gate.markApplied("session-1")
|
||||
|
||||
// #then
|
||||
expect(gate.shouldOverride("session-1")).toBe(false)
|
||||
})
|
||||
|
||||
test("ignores forked sessions", () => {
|
||||
// #given
|
||||
const gate = createFirstMessageVariantGate()
|
||||
|
||||
// #when
|
||||
gate.markSessionCreated({ id: "session-2", parentID: "session-parent" })
|
||||
|
||||
// #then
|
||||
expect(gate.shouldOverride("session-2")).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,28 @@
|
||||
type SessionInfo = {
|
||||
id?: string
|
||||
parentID?: string
|
||||
}
|
||||
|
||||
export function createFirstMessageVariantGate() {
|
||||
const pending = new Set<string>()
|
||||
|
||||
return {
|
||||
markSessionCreated(info?: SessionInfo) {
|
||||
if (info?.id && !info.parentID) {
|
||||
pending.add(info.id)
|
||||
}
|
||||
},
|
||||
shouldOverride(sessionID?: string) {
|
||||
if (!sessionID) return false
|
||||
return pending.has(sessionID)
|
||||
},
|
||||
markApplied(sessionID?: string) {
|
||||
if (!sessionID) return
|
||||
pending.delete(sessionID)
|
||||
},
|
||||
clear(sessionID?: string) {
|
||||
if (!sessionID) return
|
||||
pending.delete(sessionID)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -21,3 +21,4 @@ export * from "./opencode-version"
|
||||
export * from "./permission-compat"
|
||||
export * from "./external-plugin-detector"
|
||||
export * from "./zip-extractor"
|
||||
export * from "./agent-variant"
|
||||
|
||||
Reference in New Issue
Block a user