diff --git a/.changeset/clean-subagents-think.md b/.changeset/clean-subagents-think.md
new file mode 100644
index 00000000000..95a7e8f5fd9
--- /dev/null
+++ b/.changeset/clean-subagents-think.md
@@ -0,0 +1,6 @@
+---
+"@kilocode/cli": patch
+"kilo-code": patch
+---
+
+Support configuring the default task subagent model and reasoning effort while safely inheriting the calling agent model when the override is unavailable.
diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/models-autocomplete-open-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/models-autocomplete-open-chromium-linux.png
index e395b8b062d..06a45f9ca3c 100644
--- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/models-autocomplete-open-chromium-linux.png
+++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/models-autocomplete-open-chromium-linux.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:bdcac95e7969d021ebce19001f37f86b8ab670bd79e39086385c9419617294df
-size 27177
+oid sha256:e1a5dfe9704a74cbfa2da2a179ed995bcc31def156e798277dcfb568b25db58d
+size 35913
diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/settings-panel-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/settings-panel-chromium-linux.png
index 77079da8613..542e06b36ac 100644
--- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/settings-panel-chromium-linux.png
+++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/settings-panel-chromium-linux.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:73a34a22b24e42731510f9ecd3ea8827a0e39fa9b335294c45c4e8780df9a1f4
-size 35853
+oid sha256:6bcfc6c12f69c5131dcc8f294f480e26ff53f87e0c5943bf525e69c4c894ad21
+size 43744
diff --git a/packages/kilo-vscode/tests/unit/settings-io.test.ts b/packages/kilo-vscode/tests/unit/settings-io.test.ts
index 81e68cc7402..4ed6aaa3645 100644
--- a/packages/kilo-vscode/tests/unit/settings-io.test.ts
+++ b/packages/kilo-vscode/tests/unit/settings-io.test.ts
@@ -167,6 +167,16 @@ describe("parseImport", () => {
if (result.ok) expect(result.config.model).toBe("test-model")
})
+ it("preserves task subagent model and variant settings", () => {
+ const json = JSON.stringify({ subagent_model: "anthropic/claude-sonnet-4", subagent_variant: "high" })
+ 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")
+ }
+ })
+
it("strips _meta before returning config", () => {
const json = JSON.stringify({
_meta: { version: 1, exportedAt: "2026-01-01", secretsStripped: true },
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 c961e2d943b..9a72e0883e9 100644
--- a/packages/kilo-vscode/webview-ui/src/components/settings/ModelsTab.tsx
+++ b/packages/kilo-vscode/webview-ui/src/components/settings/ModelsTab.tsx
@@ -2,16 +2,19 @@ import { Component, For, createMemo } from "solid-js"
import { Card } from "@kilocode/kilo-ui/card"
import { useConfig } from "../../context/config"
import { useLanguage } from "../../context/language"
+import { useProvider } from "../../context/provider"
import { useSession } from "../../context/session"
import { parseModelString } from "../../../../src/shared/provider-model"
import { DEFAULT_AUTOCOMPLETE_MODEL } from "../../../../src/shared/autocomplete-models"
import { ModelSelectorBase } from "../shared/ModelSelector"
+import { ThinkingSelectorBase } from "../shared/ThinkingSelector"
import SettingsRow from "./SettingsRow"
import { AUTOCOMPLETE_PROVIDER_ID, AUTOCOMPLETE_SELECTOR_MODELS } from "./autocomplete-model-selector"
const ModelsTab: Component = () => {
const { config, settings, updateConfig, updateSetting } = useConfig()
const language = useLanguage()
+ const provider = useProvider()
const session = useSession()
const autocompleteModel = () => String(settings()["autocomplete.model"] ?? DEFAULT_AUTOCOMPLETE_MODEL.id)
@@ -26,6 +29,35 @@ 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 subagentVariant = createMemo(() => {
+ const list = subagentVariants()
+ if (list.length === 0) return undefined
+ const value = config().subagent_variant ?? undefined
+ return value && list.includes(value) ? value : undefined
+ })
+
+ function handleSubagentModelSelect(providerID: string, modelID: string) {
+ if (!providerID || !modelID) {
+ 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 })
+ }
+
+ function handleSubagentVariantSelect(value: string) {
+ updateConfig({ subagent_variant: value })
+ }
+
const allAgents = createMemo(() => session.agents())
function handleModeModelSelect(agentName: string) {
@@ -71,6 +103,26 @@ const ModelsTab: Component = () => {
includeAutoSmall
/>
+
+
+
+
+
+
= [
"permission",
"model",
"small_model",
+ "subagent_model",
+ "subagent_variant",
"default_agent",
"agent",
"provider",
diff --git a/packages/kilo-vscode/webview-ui/src/components/shared/ThinkingSelector.tsx b/packages/kilo-vscode/webview-ui/src/components/shared/ThinkingSelector.tsx
index 7af984236cc..fbea4eeea18 100644
--- a/packages/kilo-vscode/webview-ui/src/components/shared/ThinkingSelector.tsx
+++ b/packages/kilo-vscode/webview-ui/src/components/shared/ThinkingSelector.tsx
@@ -25,6 +25,8 @@ export interface ThinkingSelectorBaseProps {
onSelect: (value: string) => void
/** Delay outside dismissal while the popover opens inside a dialog. */
deferDismiss?: boolean
+ /** Popover placement — defaults to "top-start". */
+ placement?: "top-start" | "bottom-start" | "bottom-end" | "top-end"
}
export const ThinkingSelectorBase: Component = (props) => {
@@ -110,7 +112,7 @@ export const ThinkingSelectorBase: Component = (props
0}>
provider?: Record
diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts
index e2cdab1f400..300688eba6c 100644
--- a/packages/opencode/src/config/config.ts
+++ b/packages/opencode/src/config/config.ts
@@ -226,6 +226,13 @@ export const Info = Schema.Struct({
small_model: Schema.optional(Schema.NullOr(ConfigModelID)).annotate({
description: "Small model to use for tasks like title generation in the format of provider/model",
}),
+ subagent_model: Schema.optional(Schema.NullOr(ConfigModelID)).annotate({
+ description:
+ "Default model for task-tool subagents in the format of provider/model. If unset or unavailable, subagents inherit the calling agent model.",
+ }),
+ subagent_variant: Schema.optional(Schema.NullOr(Schema.String)).annotate({
+ description: "Default model variant for task-tool subagents when subagent_model is configured.",
+ }),
// kilocode_change end
// kilocode_change start - renamed from "build" to "code" + nullable for delete sentinel
default_agent: Schema.optional(Schema.NullOr(Schema.String)).annotate({
diff --git a/packages/opencode/src/kilocode/tool/task.ts b/packages/opencode/src/kilocode/tool/task.ts
index b349ab86b30..8397735c620 100644
--- a/packages/opencode/src/kilocode/tool/task.ts
+++ b/packages/opencode/src/kilocode/tool/task.ts
@@ -4,12 +4,16 @@ import path from "path"
import { Permission } from "@/permission"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Global } from "@opencode-ai/core/global"
+import * as Log from "@opencode-ai/core/util/log"
import { ModelID, ProviderID } from "@/provider/schema"
import type { Session } from "../../session/session"
import type { Agent } from "../../agent/agent"
import type { Config } from "../../config/config"
+import { Provider } from "../../provider/provider"
import z from "zod"
+const log = Log.create({ service: "kilocode-task-model" })
+
// RATIONALE: Mirror narrow state slice Task tool consumes and ignore unrelated TUI fields.
const ModelState = z
.object({
@@ -55,8 +59,20 @@ export namespace KiloTask {
return [{ permission: "task", pattern: "*", action: "deny" }, ...rules]
}
- /** Return saved CLI model for agent, if any. */
- export const resolveModel = Effect.fn("KiloTask.resolveModel")(function* (name: string) {
+ type Model = { providerID: ProviderID; modelID: ModelID }
+ type Saved = Model & { variant?: string }
+ type Choice = { model: Model; variant?: string; sticky?: boolean; direct?: boolean }
+
+ function parse(value: string | null | undefined): Model | undefined {
+ if (!value) return undefined
+ const [providerID, ...parts] = value.split("/")
+ return {
+ providerID: ProviderID.make(providerID),
+ modelID: ModelID.make(parts.join("/")),
+ }
+ }
+
+ const saved = Effect.fn("KiloTask.savedModel")(function* (name: string) {
if (Flag.KILO_CLIENT !== "cli") return undefined
const file = path.join(Global.Path.state, "model.json")
const state = yield* Effect.tryPromise({
@@ -75,4 +91,53 @@ export namespace KiloTask {
variant: state?.variant?.[`${model.providerID}/${model.modelID}`],
}
})
+
+ /** Resolve the task subagent model while discarding stale unavailable overrides. */
+ export const resolveModel = Effect.fn("KiloTask.resolveModel")(function* (input: {
+ name: string
+ agent: Pick
+ config: Pick
+ parent: Model
+ }) {
+ const state = yield* saved(input.name)
+ const cfg = parse(input.config.subagent_model)
+ const choices: Array = [
+ state
+ ? {
+ model: { providerID: state.providerID, modelID: state.modelID },
+ variant: state.variant,
+ sticky: true,
+ }
+ : undefined,
+ input.agent.model ? { model: input.agent.model, variant: input.agent.variant, direct: true } : undefined,
+ cfg ? { model: cfg, variant: input.config.subagent_variant ?? undefined } : undefined,
+ ]
+
+ for (const choice of choices) {
+ if (!choice) continue
+ if (choice.direct) return { model: choice.model, variant: choice.variant }
+ const full = yield* Effect.tryPromise(() =>
+ Provider.getModel(choice.model.providerID, choice.model.modelID),
+ ).pipe(
+ Effect.catch((err) =>
+ Effect.sync(() => {
+ log.debug("skipping unavailable task subagent model", {
+ providerID: choice.model.providerID,
+ modelID: choice.model.modelID,
+ err,
+ })
+ return undefined
+ }),
+ ),
+ )
+ if (!full) continue
+ const variant = choice.variant && full.variants?.[choice.variant] ? choice.variant : undefined
+ return {
+ model: choice.sticky && variant ? { ...choice.model, variant } : choice.model,
+ variant,
+ }
+ }
+
+ return { model: input.parent, variant: undefined }
+ })
}
diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts
index 9bb2a571bfe..24075d02847 100644
--- a/packages/opencode/src/tool/task.ts
+++ b/packages/opencode/src/tool/task.ts
@@ -118,14 +118,18 @@ export const TaskTool = Tool.define(
const msg = yield* Effect.sync(() => MessageV2.get({ sessionID: ctx.sessionID, messageID: ctx.messageID }))
if (msg.info.role !== "assistant") return yield* Effect.fail(new Error("Not an assistant message"))
- // kilocode_change start — prefer user's CLI-saved pick for this subagent
- const saved = yield* KiloTask.resolveModel(next.name)
- const model = saved ??
- next.model ?? {
+ // kilocode_change start — prefer valid subagent overrides, safely inheriting when overrides go stale
+ const selected = yield* KiloTask.resolveModel({
+ name: next.name,
+ agent: next,
+ config: cfg,
+ parent: {
modelID: msg.info.modelID,
providerID: msg.info.providerID,
- }
- const variant = saved?.variant ?? (saved ? undefined : next.variant)
+ },
+ })
+ const model = selected.model
+ const variant = selected.variant
// kilocode_change end
yield* ctx.metadata({
diff --git a/packages/opencode/test/kilocode/tool-task-model.test.ts b/packages/opencode/test/kilocode/tool-task-model.test.ts
index f87415aae97..bafe1e19cd6 100644
--- a/packages/opencode/test/kilocode/tool-task-model.test.ts
+++ b/packages/opencode/test/kilocode/tool-task-model.test.ts
@@ -48,6 +48,44 @@ const cfg = {
const savedVariant = "fast"
const cfgVariant = "balanced"
+const sub = {
+ providerID: ProviderID.make("sub-provider"),
+ modelID: ModelID.make("sub-model"),
+}
+const subVariant = "deep"
+
+function custom(id: string, model: string, variants: string[] = []) {
+ return {
+ name: id,
+ id,
+ env: [],
+ npm: "@ai-sdk/openai-compatible",
+ models: {
+ [model]: {
+ id: model,
+ name: model,
+ attachment: false,
+ reasoning: variants.length > 0,
+ temperature: false,
+ tool_call: true,
+ release_date: "2025-01-01",
+ limit: { context: 100_000, output: 10_000 },
+ cost: { input: 0, output: 0 },
+ options: {},
+ variants: Object.fromEntries(variants.map((variant) => [variant, {}])),
+ },
+ },
+ options: { apiKey: "test-key", baseURL: "http://localhost:1/v1" },
+ }
+}
+
+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]),
+ },
+}
const it = testEffect(
Layer.mergeAll(
@@ -138,7 +176,12 @@ function writeState(input: unknown) {
})
}
-function run(input: { agent: "pinned" | "worker"; state?: unknown; client?: string }) {
+function run(input: {
+ agent: "pinned" | "worker"
+ state?: unknown
+ client?: string
+ config?: Pick
+}) {
return provideTmpdirInstance(
() =>
Effect.gen(function* () {
@@ -178,6 +221,8 @@ function run(input: { agent: "pinned" | "worker"; state?: unknown; client?: stri
}),
{
config: {
+ ...catalog,
+ ...input.config,
agent: {
worker: { mode: "subagent" },
pinned: { mode: "subagent", model: "config-provider/config-model", variant: cfgVariant },
@@ -268,6 +313,70 @@ describe("tool.task model resolution", () => {
),
)
+ it.live("configured subagent default model and variant apply to task workers", () =>
+ run({
+ agent: "worker",
+ config: { subagent_model: "sub-provider/sub-model", subagent_variant: subVariant },
+ }).pipe(
+ Effect.tap((result) =>
+ Effect.sync(() => {
+ expect(result.prompt).toEqual(sub)
+ expect(result.variant).toEqual(subVariant)
+ expect(result.model).toEqual(sub)
+ expect(result.metadataVariant).toEqual(subVariant)
+ }),
+ ),
+ ),
+ )
+
+ it.live("per-agent task model remains above the configured subagent default", () =>
+ run({
+ agent: "pinned",
+ config: { subagent_model: "sub-provider/sub-model", subagent_variant: subVariant },
+ }).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", () =>
+ run({
+ agent: "worker",
+ config: { subagent_model: "missing-provider/missing-model", subagent_variant: subVariant },
+ }).pipe(
+ Effect.tap((result) =>
+ Effect.sync(() => {
+ expect(result.prompt).toEqual(parent)
+ expect(result.variant).toBeUndefined()
+ expect(result.model).toEqual(parent)
+ expect(result.metadataVariant).toBeUndefined()
+ }),
+ ),
+ ),
+ )
+
+ it.live("stale configured subagent variant is ignored without dropping its model", () =>
+ run({
+ agent: "worker",
+ config: { subagent_model: "sub-provider/sub-model", subagent_variant: "gone" },
+ }).pipe(
+ Effect.tap((result) =>
+ Effect.sync(() => {
+ expect(result.prompt).toEqual(sub)
+ expect(result.variant).toBeUndefined()
+ expect(result.model).toEqual(sub)
+ expect(result.metadataVariant).toBeUndefined()
+ }),
+ ),
+ ),
+ )
+
it.live("no file and no agent config falls back to parent for worker", () =>
run({
agent: "worker",
@@ -324,6 +433,7 @@ describe("tool.task model resolution", () => {
}),
{
config: {
+ ...catalog,
agent: {
worker: { mode: "subagent" },
pinned: { mode: "subagent", model: "config-provider/config-model", variant: cfgVariant },
diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts
index a509296e055..066d3fd4c5e 100644
--- a/packages/sdk/js/src/v2/gen/types.gen.ts
+++ b/packages/sdk/js/src/v2/gen/types.gen.ts
@@ -1326,6 +1326,8 @@ export type Config = {
terminal_command_display?: "expanded" | "collapsed"
model?: string
small_model?: string
+ subagent_model?: string
+ subagent_variant?: string
default_agent?: string
username?: string
mode?: {
diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json
index 89bd016ba98..624758a6049 100644
--- a/packages/sdk/openapi.json
+++ b/packages/sdk/openapi.json
@@ -16460,6 +16460,12 @@
"small_model": {
"type": "string"
},
+ "subagent_model": {
+ "type": "string"
+ },
+ "subagent_variant": {
+ "type": "string"
+ },
"default_agent": {
"type": "string"
},