From adf03a98245e8877c580cb1f77a7e0ea4f0af61d Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Fri, 12 Jun 2026 20:35:02 +0200 Subject: [PATCH] feat: support subagent reasoning overrides --- .changeset/bright-subagents-reason.md | 6 ++ .../tests/unit/settings-io.test.ts | 13 ++- .../src/components/settings/ModelsTab.tsx | 57 ++++++----- .../src/components/settings/settings-io.ts | 1 + .../webview-ui/src/styles/settings.css | 5 + .../webview-ui/src/types/messages/config.ts | 1 + packages/opencode/src/config/config.ts | 6 ++ packages/opencode/src/kilocode/tool/task.ts | 27 ++++- .../test/kilocode/config/config.test.ts | 37 +++++++ .../test/kilocode/tool-task-model.test.ts | 99 ++++++++++++++++++- packages/sdk/js/src/v2/gen/types.gen.ts | 3 + packages/sdk/openapi.json | 6 ++ 12 files changed, 228 insertions(+), 33 deletions(-) create mode 100644 .changeset/bright-subagents-reason.md diff --git a/.changeset/bright-subagents-reason.md b/.changeset/bright-subagents-reason.md new file mode 100644 index 000000000000..3c86f5457621 --- /dev/null +++ b/.changeset/bright-subagents-reason.md @@ -0,0 +1,6 @@ +--- +"@kilocode/cli": patch +"kilo-code": patch +--- + +Support model-specific reasoning overrides for task subagents, including custom subagents with their own model and variant settings. diff --git a/packages/kilo-vscode/tests/unit/settings-io.test.ts b/packages/kilo-vscode/tests/unit/settings-io.test.ts index 4ed6aaa3645d..ee0b838d913b 100644 --- a/packages/kilo-vscode/tests/unit/settings-io.test.ts +++ b/packages/kilo-vscode/tests/unit/settings-io.test.ts @@ -168,12 +168,23 @@ describe("parseImport", () => { }) it("preserves task subagent model and variant settings", () => { - const json = JSON.stringify({ subagent_model: "anthropic/claude-sonnet-4", subagent_variant: "high" }) + const json = JSON.stringify({ + subagent_model: "anthropic/claude-sonnet-4", + subagent_variant: "high", + subagent_variant_overrides: { + "anthropic/claude-sonnet-4": "max", + "openai/gpt-5": "xhigh", + }, + }) const result = parseImport(json) expect(result.ok).toBe(true) if (result.ok) { expect(result.config.subagent_model).toBe("anthropic/claude-sonnet-4") expect(result.config.subagent_variant).toBe("high") + expect(result.config.subagent_variant_overrides).toEqual({ + "anthropic/claude-sonnet-4": "max", + "openai/gpt-5": "xhigh", + }) } }) diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/ModelsTab.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/ModelsTab.tsx index a3fc83d1c214..4960afe249a4 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/ModelsTab.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/ModelsTab.tsx @@ -1,4 +1,4 @@ -import { Component, For, createMemo } from "solid-js" +import { Component, For, Show, createMemo } from "solid-js" import { Card } from "@kilocode/kilo-ui/card" import { useConfig } from "../../context/config" import { useLanguage } from "../../context/language" @@ -36,15 +36,14 @@ const ModelsTab: Component = () => { } const subagentModel = createMemo(() => parseModelString(config().subagent_model ?? undefined)) - const subagentVariants = createMemo(() => { - const model = provider.findModel(subagentModel()) - return model?.variants ? Object.keys(model.variants) : [] - }) + const variantKey = createMemo(() => config().subagent_model ?? undefined) + const subagentVariants = createMemo(() => Object.keys(provider.findModel(subagentModel())?.variants ?? {})) const subagentVariant = createMemo(() => { - const list = subagentVariants() - if (list.length === 0) return undefined - const value = config().subagent_variant ?? undefined - return value && list.includes(value) ? value : undefined + const key = variantKey() + if (!key) return undefined + const value = config().subagent_variant_overrides?.[key] + if (value) return value + return config().subagent_model === key ? (config().subagent_variant ?? undefined) : undefined }) function handleSubagentModelSelect(providerID: string, modelID: string) { @@ -52,16 +51,20 @@ const ModelsTab: Component = () => { updateConfig({ subagent_model: null, subagent_variant: null }) return } - const model = { providerID, modelID } - const variants = provider.findModel(model)?.variants - const list = variants ? Object.keys(variants) : [] - const value = config().subagent_model === `${providerID}/${modelID}` ? config().subagent_variant : undefined - const variant = value && list.includes(value) ? value : list[0] - updateConfig({ subagent_model: `${providerID}/${modelID}`, subagent_variant: variant ?? null }) + const value = `${providerID}/${modelID}` + updateConfig({ + subagent_model: value, + ...(config().subagent_model === value ? {} : { subagent_variant: null }), + }) } - function handleSubagentVariantSelect(value: string) { - updateConfig({ subagent_variant: value }) + function updateSubagentVariant(value: string | null) { + const key = variantKey() + if (!key) return + updateConfig({ + subagent_variant_overrides: { [key]: value }, + ...(config().subagent_model === key ? { subagent_variant: null } : {}), + }) } const allAgents = createMemo(() => session.agents()) @@ -124,7 +127,7 @@ const ModelsTab: Component = () => { title={language.t("settings.providers.subagentModel.title")} description={language.t("settings.providers.subagentModel.description")} > -
+
{ label={language.t("settings.providers.subagentModel.title")} description={language.t("settings.providers.subagentModel.description")} /> - + 0}> + updateSubagentVariant(value)} + onClear={() => updateSubagentVariant(null)} + allowClear + clearLabel={language.t("settings.providers.notSet")} + placement="bottom-start" + globalTrigger={false} + /> +
= [ "small_model", "subagent_model", "subagent_variant", + "subagent_variant_overrides", "default_agent", "agent", "provider", diff --git a/packages/kilo-vscode/webview-ui/src/styles/settings.css b/packages/kilo-vscode/webview-ui/src/styles/settings.css index d6371643cc06..fac12d5d01aa 100644 --- a/packages/kilo-vscode/webview-ui/src/styles/settings.css +++ b/packages/kilo-vscode/webview-ui/src/styles/settings.css @@ -107,6 +107,11 @@ & > * { max-width: 100%; } + + [data-slot="popover-trigger"] { + min-width: 0; + max-width: 100%; + } } } diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/config.ts b/packages/kilo-vscode/webview-ui/src/types/messages/config.ts index 21b156edce1c..74022ce09a16 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/config.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/config.ts @@ -116,6 +116,7 @@ export interface Config { small_model?: string | null subagent_model?: string | null subagent_variant?: string | null + subagent_variant_overrides?: Record | null default_agent?: string | null agent?: Record provider?: Record diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 44ff4f4f0914..07ccb88b4162 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -237,6 +237,12 @@ export const Info = Schema.Struct({ subagent_variant: Schema.optional(Schema.NullOr(Schema.String)).annotate({ description: "Default model variant for task-tool subagents when subagent_model is configured.", }), + subagent_variant_overrides: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.NullOr(Schema.String))), + ).annotate({ + description: + "Model-specific variant overrides for task-tool subagents, keyed by provider/model. Valid overrides take precedence over saved, agent-specific, and inherited variants.", + }), default_agent: Schema.optional(Schema.NullOr(Schema.String)).annotate({ description: "Default agent to use when none is specified. Must be a primary agent. Falls back to 'code' if not set or if the specified agent is invalid.", diff --git a/packages/opencode/src/kilocode/tool/task.ts b/packages/opencode/src/kilocode/tool/task.ts index 1dd405d1b2a8..8590ebb87af3 100644 --- a/packages/opencode/src/kilocode/tool/task.ts +++ b/packages/opencode/src/kilocode/tool/task.ts @@ -84,6 +84,10 @@ export namespace KiloTask { type Saved = Model & { variant?: string } type Choice = { model: Model; variant?: string; sticky?: boolean; direct?: boolean } + function key(model: Model) { + return `${model.providerID}/${model.modelID}` + } + function parse(value: string | null | undefined): Model | undefined { if (!value) return undefined const [providerID, ...parts] = value.split("/") @@ -117,13 +121,14 @@ export namespace KiloTask { export const resolveModel = Effect.fn("KiloTask.resolveModel")(function* (input: { name: string agent: Pick - config: Pick + config: Pick parent: Model variant?: string provider: Provider.Interface }) { const state = yield* saved(input.name) const cfg = parse(input.config.subagent_model) + const override = (model: Model) => input.config.subagent_variant_overrides?.[key(model)] ?? undefined const choices: Array = [ state ? { @@ -138,7 +143,13 @@ export namespace KiloTask { for (const choice of choices) { if (!choice) continue - if (choice.direct) return { model: choice.model, variant: choice.variant } + if (choice.direct) { + const value = override(choice.model) + if (!value) return { model: choice.model, variant: choice.variant } + const full = yield* input.provider.getModel(choice.model.providerID, choice.model.modelID) + const variant = full.variants?.[value] ? value : choice.variant + return { model: choice.model, variant } + } const full = yield* input.provider.getModel(choice.model.providerID, choice.model.modelID).pipe( Effect.catchTag("ProviderModelNotFoundError", (err) => Effect.sync(() => { @@ -152,13 +163,21 @@ export namespace KiloTask { ), ) if (!full) continue - const variant = choice.variant && full.variants?.[choice.variant] ? choice.variant : undefined + const fallback = choice.variant && full.variants?.[choice.variant] ? choice.variant : undefined + const value = override(choice.model) + const variant = value && full.variants?.[value] ? value : fallback return { model: choice.sticky && variant ? { ...choice.model, variant } : choice.model, variant, } } - return { model: input.parent, variant: input.variant } + const value = override(input.parent) + if (!value) return { model: input.parent, variant: input.variant } + const full = yield* input.provider + .getModel(input.parent.providerID, input.parent.modelID) + .pipe(Effect.catchTag("ProviderModelNotFoundError", () => Effect.succeed(undefined))) + const variant = full?.variants?.[value] ? value : input.variant + return { model: input.parent, variant } }) } diff --git a/packages/opencode/test/kilocode/config/config.test.ts b/packages/opencode/test/kilocode/config/config.test.ts index a1dd2fb26cb1..118938b3e114 100644 --- a/packages/opencode/test/kilocode/config/config.test.ts +++ b/packages/opencode/test/kilocode/config/config.test.ts @@ -234,6 +234,43 @@ describe("kilocode indexing config", () => { }) }) +describe("subagent variant overrides", () => { + test("removes one model override without removing sibling models", () => { + const patch = decode({ + subagent_variant_overrides: { + "anthropic/claude-sonnet-4-6": null, + }, + }) + const merged = KilocodeConfig.mergeConfig( + { + subagent_variant_overrides: { + "anthropic/claude-sonnet-4-6": "high", + "openai/gpt-5": "xhigh", + }, + }, + patch, + ) + + expect(patch.subagent_variant_overrides?.["anthropic/claude-sonnet-4-6"]).toBeNull() + expect(merged.subagent_variant_overrides).toEqual({ "openai/gpt-5": "xhigh" }) + }) + + test("accepts a delete sentinel for the complete override map", () => { + const patch = decode({ subagent_variant_overrides: null }) + const merged = KilocodeConfig.mergeConfig( + { + subagent_variant_overrides: { + "anthropic/claude-sonnet-4-6": "high", + }, + }, + patch, + ) + + expect(patch.subagent_variant_overrides).toBeNull() + expect(merged.subagent_variant_overrides).toBeUndefined() + }) +}) + describe("agent config", () => { test("accepts delete sentinels for agent model and variant overrides", () => { const patch = decode({ agent: { explore: { model: null, variant: null } } }) diff --git a/packages/opencode/test/kilocode/tool-task-model.test.ts b/packages/opencode/test/kilocode/tool-task-model.test.ts index 3ec6979e6361..7e0770c28ccb 100644 --- a/packages/opencode/test/kilocode/tool-task-model.test.ts +++ b/packages/opencode/test/kilocode/tool-task-model.test.ts @@ -53,6 +53,7 @@ const cfg = { } const inherited = "thorough" +const overrideVariant = "full" const savedVariant = "fast" const cfgVariant = "balanced" const sub = { @@ -88,9 +89,10 @@ function custom(id: string, model: string, variants: string[] = []) { const catalog = { provider: { - "saved-provider": custom("saved-provider", "saved-model", [savedVariant]), - "config-provider": custom("config-provider", "config-model", [cfgVariant]), - "sub-provider": custom("sub-provider", "sub-model", [subVariant]), + "parent-provider": custom("parent-provider", "parent-model", [inherited, overrideVariant]), + "saved-provider": custom("saved-provider", "saved-model", [savedVariant, overrideVariant]), + "config-provider": custom("config-provider", "config-model", [cfgVariant, overrideVariant]), + "sub-provider": custom("sub-provider", "sub-model", [subVariant, overrideVariant]), }, } @@ -197,7 +199,7 @@ function run(input: { state?: unknown client?: string variant?: string - config?: Pick + config?: Pick }) { return provideTmpdirInstance( () => @@ -365,6 +367,95 @@ describe("tool.task model resolution", () => { ), ) + it.live("model-specific override replaces an inherited parent variant", () => + run({ + agent: "worker", + variant: inherited, + config: { subagent_variant_overrides: { "parent-provider/parent-model": overrideVariant } }, + }).pipe( + Effect.tap((result) => + Effect.sync(() => { + expect(result.prompt).toEqual(parent) + expect(result.variant).toEqual(overrideVariant) + expect(result.model).toEqual(parent) + expect(result.metadataVariant).toEqual(overrideVariant) + }), + ), + ), + ) + + it.live("model-specific override applies to a custom subagent model and variant", () => + run({ + agent: "pinned", + variant: inherited, + config: { subagent_variant_overrides: { "config-provider/config-model": overrideVariant } }, + }).pipe( + Effect.tap((result) => + Effect.sync(() => { + expect(result.prompt).toEqual(cfg) + expect(result.variant).toEqual(overrideVariant) + expect(result.model).toEqual(cfg) + expect(result.metadataVariant).toEqual(overrideVariant) + }), + ), + ), + ) + + it.live("model-specific override follows a saved custom subagent model", () => + run({ + agent: "worker", + state: { model: { worker: saved }, variant: { "saved-provider/saved-model": savedVariant } }, + config: { subagent_variant_overrides: { "saved-provider/saved-model": overrideVariant } }, + }).pipe( + Effect.tap((result) => + Effect.sync(() => { + expect(result.prompt).toEqual(saved) + expect(result.variant).toEqual(overrideVariant) + expect(result.model).toMatchObject({ ...saved, variant: overrideVariant }) + expect(result.metadataVariant).toEqual(overrideVariant) + }), + ), + ), + ) + + it.live("stale model-specific override preserves the resolved variant", () => + run({ + agent: "pinned", + variant: inherited, + config: { subagent_variant_overrides: { "config-provider/config-model": "gone" } }, + }).pipe( + Effect.tap((result) => + Effect.sync(() => { + expect(result.prompt).toEqual(cfg) + expect(result.variant).toEqual(cfgVariant) + expect(result.model).toEqual(cfg) + expect(result.metadataVariant).toEqual(cfgVariant) + }), + ), + ), + ) + + it.live("unavailable configured subagent model falls back to the parent model override", () => + run({ + agent: "worker", + variant: inherited, + config: { + subagent_model: "missing-provider/missing-model", + subagent_variant: subVariant, + subagent_variant_overrides: { "parent-provider/parent-model": overrideVariant }, + }, + }).pipe( + Effect.tap((result) => + Effect.sync(() => { + expect(result.prompt).toEqual(parent) + expect(result.variant).toEqual(overrideVariant) + expect(result.model).toEqual(parent) + expect(result.metadataVariant).toEqual(overrideVariant) + }), + ), + ), + ) + it.live("unavailable configured subagent model falls back to the parent model", () => run({ agent: "worker", diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index af264d90b779..6d3f2a48e99b 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1332,6 +1332,9 @@ export type Config = { small_model?: string subagent_model?: string subagent_variant?: string + subagent_variant_overrides?: { + [key: string]: string + } default_agent?: string username?: string mode?: { diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 4d66ec8d8ba4..5763b694d176 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -18227,6 +18227,12 @@ "subagent_variant": { "type": "string" }, + "subagent_variant_overrides": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, "default_agent": { "type": "string" },