Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/bright-subagents-reason.md
Original file line number Diff line number Diff line change
@@ -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.
13 changes: 12 additions & 1 deletion packages/kilo-vscode/tests/unit/settings-io.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
})
}
})

Expand Down
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -36,32 +36,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 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) {
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 })
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())
Expand Down Expand Up @@ -124,7 +127,7 @@ const ModelsTab: Component = () => {
title={language.t("settings.providers.subagentModel.title")}
description={language.t("settings.providers.subagentModel.description")}
>
<div style={{ display: "flex", "align-items": "center", gap: "8px", "flex-wrap": "wrap" }}>
<div style={{ display: "flex", "flex-direction": "column", "align-items": "flex-end", gap: "8px" }}>
<ModelSelectorBase
value={subagentModel()}
onSelect={handleSubagentModelSelect}
Expand All @@ -134,12 +137,18 @@ const ModelsTab: Component = () => {
label={language.t("settings.providers.subagentModel.title")}
description={language.t("settings.providers.subagentModel.description")}
/>
<ThinkingSelectorBase
variants={subagentVariants()}
value={subagentVariant()}
onSelect={handleSubagentVariantSelect}
placement="bottom-start"
/>
<Show when={subagentVariants().length > 0}>
<ThinkingSelectorBase
variants={subagentVariants()}
value={subagentVariant()}
onSelect={(value) => updateSubagentVariant(value)}
onClear={() => updateSubagentVariant(null)}
allowClear
clearLabel={language.t("settings.providers.notSet")}
placement="bottom-start"
globalTrigger={false}
/>
</Show>
</div>
</SettingsRow>
<SettingsRow
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const KNOWN_KEYS: ReadonlyArray<string> = [
"small_model",
"subagent_model",
"subagent_variant",
"subagent_variant_overrides",
"default_agent",
"agent",
"provider",
Expand Down
5 changes: 5 additions & 0 deletions packages/kilo-vscode/webview-ui/src/styles/settings.css
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,11 @@
& > * {
max-width: 100%;
}

[data-slot="popover-trigger"] {
min-width: 0;
max-width: 100%;
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ export interface Config {
small_model?: string | null
subagent_model?: string | null
subagent_variant?: string | null
subagent_variant_overrides?: Record<string, string | null> | null
default_agent?: string | null
agent?: Record<string, AgentConfig>
provider?: Record<string, ProviderConfig>
Expand Down
6 changes: 6 additions & 0 deletions packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
27 changes: 23 additions & 4 deletions packages/opencode/src/kilocode/tool/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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("/")
Expand Down Expand Up @@ -117,13 +121,14 @@ export namespace KiloTask {
export const resolveModel = Effect.fn("KiloTask.resolveModel")(function* (input: {
name: string
agent: Pick<Agent.Info, "model" | "variant">
config: Pick<Config.Info, "subagent_model" | "subagent_variant">
config: Pick<Config.Info, "subagent_model" | "subagent_variant" | "subagent_variant_overrides">
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<Choice | undefined> = [
state
? {
Expand All @@ -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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: getModel is called here without a ProviderModelNotFoundError handler, unlike the adjacent non-direct path (lines 153–164) and the parent fallback path (lines 177–179).

If an agent has model hard-coded in its config and the provider later removes or renames that model, and the user has set a subagent_variant_override for it, this call will propagate an unhandled ProviderModelNotFoundError rather than gracefully falling back to choice.variant.

The fix mirrors the parent fallback:

const full = yield* input.provider
  .getModel(choice.model.providerID, choice.model.modelID)
  .pipe(Effect.catchTag("ProviderModelNotFoundError", () => Effect.succeed(undefined)))
const variant = full?.variants?.[value] ? value : choice.variant
return { model: choice.model, variant }

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

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(() => {
Expand All @@ -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 }
})
}
37 changes: 37 additions & 0 deletions packages/opencode/test/kilocode/config/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 } } })
Expand Down
Loading
Loading