fix(config): plan agent inherits model settings from prometheus when not explicitly configured
Previously, demoted plan agent only received { mode: 'subagent' } with no
model settings, causing fallback to step-3.5-flash. Now inherits all
model-related settings (model, variant, temperature, top_p, maxTokens,
thinking, reasoningEffort, textVerbosity, providerOptions) from the
resolved prometheus config. User overrides via agents.plan.* take priority.
Prompt, permission, description, and color are intentionally NOT inherited.
This commit is contained in:
parent
71ac54c33e
commit
b88a868173
@ -600,6 +600,187 @@ describe("Prometheus direct override priority over category", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("Plan agent model inheritance from prometheus", () => {
|
||||||
|
test("plan agent inherits all model-related settings from resolved prometheus config", async () => {
|
||||||
|
//#given - prometheus resolves to claude-opus-4-6 with model settings
|
||||||
|
spyOn(shared, "resolveModelPipeline" as any).mockReturnValue({
|
||||||
|
model: "anthropic/claude-opus-4-6",
|
||||||
|
provenance: "provider-fallback",
|
||||||
|
variant: "max",
|
||||||
|
})
|
||||||
|
const pluginConfig: OhMyOpenCodeConfig = {
|
||||||
|
sisyphus_agent: {
|
||||||
|
planner_enabled: true,
|
||||||
|
replace_plan: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const config: Record<string, unknown> = {
|
||||||
|
model: "anthropic/claude-opus-4-6",
|
||||||
|
agent: {
|
||||||
|
plan: {
|
||||||
|
name: "plan",
|
||||||
|
mode: "primary",
|
||||||
|
prompt: "original plan prompt",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const handler = createConfigHandler({
|
||||||
|
ctx: { directory: "/tmp" },
|
||||||
|
pluginConfig,
|
||||||
|
modelCacheState: {
|
||||||
|
anthropicContext1MEnabled: false,
|
||||||
|
modelContextLimitsCache: new Map(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
//#when
|
||||||
|
await handler(config)
|
||||||
|
|
||||||
|
//#then - plan inherits model and variant from prometheus, but NOT prompt
|
||||||
|
const agents = config.agent as Record<string, { mode?: string; model?: string; variant?: string; prompt?: string }>
|
||||||
|
expect(agents.plan).toBeDefined()
|
||||||
|
expect(agents.plan.mode).toBe("subagent")
|
||||||
|
expect(agents.plan.model).toBe("anthropic/claude-opus-4-6")
|
||||||
|
expect(agents.plan.variant).toBe("max")
|
||||||
|
expect(agents.plan.prompt).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("plan agent inherits temperature, reasoningEffort, and other model settings from prometheus", async () => {
|
||||||
|
//#given - prometheus configured with category that has temperature and reasoningEffort
|
||||||
|
spyOn(shared, "resolveModelPipeline" as any).mockReturnValue({
|
||||||
|
model: "openai/gpt-5.2",
|
||||||
|
provenance: "override",
|
||||||
|
variant: "high",
|
||||||
|
})
|
||||||
|
const pluginConfig: OhMyOpenCodeConfig = {
|
||||||
|
sisyphus_agent: {
|
||||||
|
planner_enabled: true,
|
||||||
|
replace_plan: true,
|
||||||
|
},
|
||||||
|
agents: {
|
||||||
|
prometheus: {
|
||||||
|
model: "openai/gpt-5.2",
|
||||||
|
variant: "high",
|
||||||
|
temperature: 0.3,
|
||||||
|
top_p: 0.9,
|
||||||
|
maxTokens: 16000,
|
||||||
|
reasoningEffort: "high",
|
||||||
|
textVerbosity: "medium",
|
||||||
|
thinking: { type: "enabled", budgetTokens: 8000 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const config: Record<string, unknown> = {
|
||||||
|
model: "anthropic/claude-opus-4-6",
|
||||||
|
agent: {},
|
||||||
|
}
|
||||||
|
const handler = createConfigHandler({
|
||||||
|
ctx: { directory: "/tmp" },
|
||||||
|
pluginConfig,
|
||||||
|
modelCacheState: {
|
||||||
|
anthropicContext1MEnabled: false,
|
||||||
|
modelContextLimitsCache: new Map(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
//#when
|
||||||
|
await handler(config)
|
||||||
|
|
||||||
|
//#then - plan inherits ALL model-related settings from resolved prometheus
|
||||||
|
const agents = config.agent as Record<string, Record<string, unknown>>
|
||||||
|
expect(agents.plan).toBeDefined()
|
||||||
|
expect(agents.plan.mode).toBe("subagent")
|
||||||
|
expect(agents.plan.model).toBe("openai/gpt-5.2")
|
||||||
|
expect(agents.plan.variant).toBe("high")
|
||||||
|
expect(agents.plan.temperature).toBe(0.3)
|
||||||
|
expect(agents.plan.top_p).toBe(0.9)
|
||||||
|
expect(agents.plan.maxTokens).toBe(16000)
|
||||||
|
expect(agents.plan.reasoningEffort).toBe("high")
|
||||||
|
expect(agents.plan.textVerbosity).toBe("medium")
|
||||||
|
expect(agents.plan.thinking).toEqual({ type: "enabled", budgetTokens: 8000 })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("plan agent user override takes priority over prometheus inherited settings", async () => {
|
||||||
|
//#given - prometheus resolves to opus, but user has plan override for gpt-5.2
|
||||||
|
spyOn(shared, "resolveModelPipeline" as any).mockReturnValue({
|
||||||
|
model: "anthropic/claude-opus-4-6",
|
||||||
|
provenance: "provider-fallback",
|
||||||
|
variant: "max",
|
||||||
|
})
|
||||||
|
const pluginConfig: OhMyOpenCodeConfig = {
|
||||||
|
sisyphus_agent: {
|
||||||
|
planner_enabled: true,
|
||||||
|
replace_plan: true,
|
||||||
|
},
|
||||||
|
agents: {
|
||||||
|
plan: {
|
||||||
|
model: "openai/gpt-5.2",
|
||||||
|
variant: "high",
|
||||||
|
temperature: 0.5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const config: Record<string, unknown> = {
|
||||||
|
model: "anthropic/claude-opus-4-6",
|
||||||
|
agent: {},
|
||||||
|
}
|
||||||
|
const handler = createConfigHandler({
|
||||||
|
ctx: { directory: "/tmp" },
|
||||||
|
pluginConfig,
|
||||||
|
modelCacheState: {
|
||||||
|
anthropicContext1MEnabled: false,
|
||||||
|
modelContextLimitsCache: new Map(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
//#when
|
||||||
|
await handler(config)
|
||||||
|
|
||||||
|
//#then - plan uses its own override, not prometheus settings
|
||||||
|
const agents = config.agent as Record<string, Record<string, unknown>>
|
||||||
|
expect(agents.plan.model).toBe("openai/gpt-5.2")
|
||||||
|
expect(agents.plan.variant).toBe("high")
|
||||||
|
expect(agents.plan.temperature).toBe(0.5)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("plan agent does NOT inherit prompt, description, or color from prometheus", async () => {
|
||||||
|
//#given
|
||||||
|
spyOn(shared, "resolveModelPipeline" as any).mockReturnValue({
|
||||||
|
model: "anthropic/claude-opus-4-6",
|
||||||
|
provenance: "provider-fallback",
|
||||||
|
variant: "max",
|
||||||
|
})
|
||||||
|
const pluginConfig: OhMyOpenCodeConfig = {
|
||||||
|
sisyphus_agent: {
|
||||||
|
planner_enabled: true,
|
||||||
|
replace_plan: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const config: Record<string, unknown> = {
|
||||||
|
model: "anthropic/claude-opus-4-6",
|
||||||
|
agent: {},
|
||||||
|
}
|
||||||
|
const handler = createConfigHandler({
|
||||||
|
ctx: { directory: "/tmp" },
|
||||||
|
pluginConfig,
|
||||||
|
modelCacheState: {
|
||||||
|
anthropicContext1MEnabled: false,
|
||||||
|
modelContextLimitsCache: new Map(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
//#when
|
||||||
|
await handler(config)
|
||||||
|
|
||||||
|
//#then - plan has model settings but NOT prompt/description/color
|
||||||
|
const agents = config.agent as Record<string, Record<string, unknown>>
|
||||||
|
expect(agents.plan.model).toBe("anthropic/claude-opus-4-6")
|
||||||
|
expect(agents.plan.prompt).toBeUndefined()
|
||||||
|
expect(agents.plan.description).toBeUndefined()
|
||||||
|
expect(agents.plan.color).toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe("Deadlock prevention - fetchAvailableModels must not receive client", () => {
|
describe("Deadlock prevention - fetchAvailableModels must not receive client", () => {
|
||||||
test("fetchAvailableModels should be called with undefined client to prevent deadlock during plugin init", async () => {
|
test("fetchAvailableModels should be called with undefined client to prevent deadlock during plugin init", async () => {
|
||||||
// given - This test ensures we don't regress on issue #1301
|
// given - This test ensures we don't regress on issue #1301
|
||||||
|
|||||||
@ -32,6 +32,7 @@ import { AGENT_NAME_MAP } from "../shared/migration";
|
|||||||
import { AGENT_MODEL_REQUIREMENTS } from "../shared/model-requirements";
|
import { AGENT_MODEL_REQUIREMENTS } from "../shared/model-requirements";
|
||||||
import { PROMETHEUS_SYSTEM_PROMPT, PROMETHEUS_PERMISSION } from "../agents/prometheus";
|
import { PROMETHEUS_SYSTEM_PROMPT, PROMETHEUS_PERMISSION } from "../agents/prometheus";
|
||||||
import { DEFAULT_CATEGORIES } from "../tools/delegate-task/constants";
|
import { DEFAULT_CATEGORIES } from "../tools/delegate-task/constants";
|
||||||
|
import { buildPlanDemoteConfig } from "./plan-model-inheritance";
|
||||||
import type { ModelCacheState } from "../plugin-state";
|
import type { ModelCacheState } from "../plugin-state";
|
||||||
import type { CategoryConfig } from "../config/schema";
|
import type { CategoryConfig } from "../config/schema";
|
||||||
|
|
||||||
@ -385,8 +386,10 @@ export function createConfigHandler(deps: ConfigHandlerDeps) {
|
|||||||
: {};
|
: {};
|
||||||
|
|
||||||
const planDemoteConfig = shouldDemotePlan
|
const planDemoteConfig = shouldDemotePlan
|
||||||
? { mode: "subagent" as const
|
? buildPlanDemoteConfig(
|
||||||
}
|
agentConfig["prometheus"] as Record<string, unknown> | undefined,
|
||||||
|
pluginConfig.agents?.plan as Record<string, unknown> | undefined,
|
||||||
|
)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
config.agent = {
|
config.agent = {
|
||||||
|
|||||||
118
src/plugin-handlers/plan-model-inheritance.test.ts
Normal file
118
src/plugin-handlers/plan-model-inheritance.test.ts
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
import { describe, test, expect } from "bun:test"
|
||||||
|
import { buildPlanDemoteConfig } from "./plan-model-inheritance"
|
||||||
|
|
||||||
|
describe("buildPlanDemoteConfig", () => {
|
||||||
|
test("returns only mode when prometheus and plan override are both undefined", () => {
|
||||||
|
//#given
|
||||||
|
const prometheusConfig = undefined
|
||||||
|
const planOverride = undefined
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = buildPlanDemoteConfig(prometheusConfig, planOverride)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result).toEqual({ mode: "subagent" })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("extracts all model settings from prometheus config", () => {
|
||||||
|
//#given
|
||||||
|
const prometheusConfig = {
|
||||||
|
name: "prometheus",
|
||||||
|
model: "anthropic/claude-opus-4-6",
|
||||||
|
variant: "max",
|
||||||
|
mode: "all",
|
||||||
|
prompt: "You are Prometheus...",
|
||||||
|
permission: { edit: "allow" },
|
||||||
|
description: "Plan agent (Prometheus)",
|
||||||
|
color: "#FF5722",
|
||||||
|
temperature: 0.1,
|
||||||
|
top_p: 0.95,
|
||||||
|
maxTokens: 32000,
|
||||||
|
thinking: { type: "enabled", budgetTokens: 10000 },
|
||||||
|
reasoningEffort: "high",
|
||||||
|
textVerbosity: "medium",
|
||||||
|
providerOptions: { key: "value" },
|
||||||
|
}
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = buildPlanDemoteConfig(prometheusConfig, undefined)
|
||||||
|
|
||||||
|
//#then - picks model settings, NOT prompt/permission/description/color/name/mode
|
||||||
|
expect(result.mode).toBe("subagent")
|
||||||
|
expect(result.model).toBe("anthropic/claude-opus-4-6")
|
||||||
|
expect(result.variant).toBe("max")
|
||||||
|
expect(result.temperature).toBe(0.1)
|
||||||
|
expect(result.top_p).toBe(0.95)
|
||||||
|
expect(result.maxTokens).toBe(32000)
|
||||||
|
expect(result.thinking).toEqual({ type: "enabled", budgetTokens: 10000 })
|
||||||
|
expect(result.reasoningEffort).toBe("high")
|
||||||
|
expect(result.textVerbosity).toBe("medium")
|
||||||
|
expect(result.providerOptions).toEqual({ key: "value" })
|
||||||
|
expect(result.prompt).toBeUndefined()
|
||||||
|
expect(result.permission).toBeUndefined()
|
||||||
|
expect(result.description).toBeUndefined()
|
||||||
|
expect(result.color).toBeUndefined()
|
||||||
|
expect(result.name).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("plan override takes priority over prometheus for all model settings", () => {
|
||||||
|
//#given
|
||||||
|
const prometheusConfig = {
|
||||||
|
model: "anthropic/claude-opus-4-6",
|
||||||
|
variant: "max",
|
||||||
|
temperature: 0.1,
|
||||||
|
reasoningEffort: "high",
|
||||||
|
}
|
||||||
|
const planOverride = {
|
||||||
|
model: "openai/gpt-5.2",
|
||||||
|
variant: "high",
|
||||||
|
temperature: 0.5,
|
||||||
|
reasoningEffort: "low",
|
||||||
|
}
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = buildPlanDemoteConfig(prometheusConfig, planOverride)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result.model).toBe("openai/gpt-5.2")
|
||||||
|
expect(result.variant).toBe("high")
|
||||||
|
expect(result.temperature).toBe(0.5)
|
||||||
|
expect(result.reasoningEffort).toBe("low")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("falls back to prometheus when plan override has partial settings", () => {
|
||||||
|
//#given
|
||||||
|
const prometheusConfig = {
|
||||||
|
model: "anthropic/claude-opus-4-6",
|
||||||
|
variant: "max",
|
||||||
|
temperature: 0.1,
|
||||||
|
reasoningEffort: "high",
|
||||||
|
}
|
||||||
|
const planOverride = {
|
||||||
|
model: "openai/gpt-5.2",
|
||||||
|
}
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = buildPlanDemoteConfig(prometheusConfig, planOverride)
|
||||||
|
|
||||||
|
//#then - plan model wins, rest inherits from prometheus
|
||||||
|
expect(result.model).toBe("openai/gpt-5.2")
|
||||||
|
expect(result.variant).toBe("max")
|
||||||
|
expect(result.temperature).toBe(0.1)
|
||||||
|
expect(result.reasoningEffort).toBe("high")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("skips undefined values from both sources", () => {
|
||||||
|
//#given
|
||||||
|
const prometheusConfig = {
|
||||||
|
model: "anthropic/claude-opus-4-6",
|
||||||
|
}
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = buildPlanDemoteConfig(prometheusConfig, undefined)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result).toEqual({ mode: "subagent", model: "anthropic/claude-opus-4-6" })
|
||||||
|
expect(Object.keys(result)).toEqual(["mode", "model"])
|
||||||
|
})
|
||||||
|
})
|
||||||
27
src/plugin-handlers/plan-model-inheritance.ts
Normal file
27
src/plugin-handlers/plan-model-inheritance.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
const MODEL_SETTINGS_KEYS = [
|
||||||
|
"model",
|
||||||
|
"variant",
|
||||||
|
"temperature",
|
||||||
|
"top_p",
|
||||||
|
"maxTokens",
|
||||||
|
"thinking",
|
||||||
|
"reasoningEffort",
|
||||||
|
"textVerbosity",
|
||||||
|
"providerOptions",
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export function buildPlanDemoteConfig(
|
||||||
|
prometheusConfig: Record<string, unknown> | undefined,
|
||||||
|
planOverride: Record<string, unknown> | undefined,
|
||||||
|
): Record<string, unknown> {
|
||||||
|
const modelSettings: Record<string, unknown> = {}
|
||||||
|
|
||||||
|
for (const key of MODEL_SETTINGS_KEYS) {
|
||||||
|
const value = planOverride?.[key] ?? prometheusConfig?.[key]
|
||||||
|
if (value !== undefined) {
|
||||||
|
modelSettings[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { mode: "subagent" as const, ...modelSettings }
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user