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"
},