fix(vscode): allow removing provider reasoning (#11238)

* fix(vscode): allow removing provider reasoning

* style(vscode): format provider regression test

---------

Co-authored-by: Christiaan Arnoldus <christiaan.arnoldus@outlook.com>
This commit is contained in:
Marius
2026-06-15 12:00:37 +02:00
committed by GitHub
parent 7f886c75ef
commit fb2db2e018
6 changed files with 98 additions and 23 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---
Allow reasoning to be removed from custom provider models after it has been enabled.
@@ -149,13 +149,23 @@ export function sanitizeCustomProviderConfig(provider: unknown): { value: Saniti
}
type AnyRecord = Record<string, unknown>
type ProviderPatch = Omit<SanitizedProviderConfig, "models"> & {
models: Record<
string,
null | {
name: string
reasoning?: true | null
variants?: Record<string, VariantConfig | null>
}
>
}
function isRecord(v: unknown): v is AnyRecord {
return !!v && typeof v === "object" && !Array.isArray(v)
}
/**
* Build a provider patch that includes null sentinels for models and variants
* Build a provider patch that includes null sentinels for model properties
* that existed in the previous config but are absent from the new one. The CLI
* `config.update` endpoint deep-merges the payload with the existing config;
* without explicit nulls, removed entries would persist on disk.
@@ -163,7 +173,7 @@ function isRecord(v: unknown): v is AnyRecord {
export function withCustomProviderDeletions(existing: unknown, next: SanitizedProviderConfig): SanitizedProviderConfig {
if (!isRecord(existing)) return next
const oldModels = isRecord(existing.models) ? existing.models : {}
const patched: AnyRecord = { ...next.models }
const patched: ProviderPatch["models"] = { ...next.models }
for (const id of Object.keys(oldModels)) {
if (!(id in patched)) {
@@ -171,15 +181,21 @@ export function withCustomProviderDeletions(existing: unknown, next: SanitizedPr
continue
}
const oldModel = oldModels[id]
const oldVariants = isRecord(oldModel) && isRecord(oldModel.variants) ? oldModel.variants : {}
const newModel = patched[id]
if (!isRecord(newModel)) continue
if (!isRecord(oldModel) || !isRecord(newModel)) continue
const oldVariants = isRecord(oldModel.variants) ? oldModel.variants : {}
const newVariants = isRecord(newModel.variants) ? newModel.variants : {}
const removedVariants = Object.keys(oldVariants).filter((v) => !(v in newVariants))
if (removedVariants.length === 0) continue
const nulls = Object.fromEntries(removedVariants.map((v) => [v, null]))
patched[id] = { ...newModel, variants: { ...newVariants, ...nulls } }
const removed = Object.keys(oldVariants).filter((variant) => !(variant in newVariants))
const variants =
removed.length > 0
? { ...newVariants, ...Object.fromEntries(removed.map((variant) => [variant, null])) }
: newModel.variants
patched[id] = {
...newModel,
...(variants ? { variants } : {}),
...(oldModel.reasoning !== undefined && newModel.reasoning === undefined ? { reasoning: null } : {}),
}
}
return { ...next, models: patched as SanitizedProviderConfig["models"] }
return { ...next, models: patched } as SanitizedProviderConfig
}
@@ -166,11 +166,12 @@ describe("withCustomProviderDeletions", () => {
expect(models.gone).toBeNull()
})
it("emits null for variants removed from a surviving model", () => {
it("emits null for reasoning and variants removed from a surviving model", () => {
const existing = {
models: {
keep: {
name: "Keep",
reasoning: true,
variants: { high: { reasoningEffort: "high" }, low: { reasoningEffort: "low" } },
},
},
@@ -182,9 +183,11 @@ describe("withCustomProviderDeletions", () => {
},
} as typeof baseNext
const result = withCustomProviderDeletions(existing, next)
const model = (result.models as Record<string, { variants: Record<string, unknown> }>).keep
expect(model.variants.high).toEqual({ reasoningEffort: "high" })
expect(model.variants.low).toBeNull()
const model = (result.models as Record<string, { reasoning?: boolean | null; variants?: Record<string, unknown> }>)
.keep
expect(model.reasoning).toBeNull()
expect(model.variants?.high).toEqual({ reasoningEffort: "high" })
expect(model.variants?.low).toBeNull()
})
it("does not touch variants on a model that is being deleted", () => {
@@ -243,7 +243,7 @@ describe("saveCustomProvider", () => {
expect(payload.myprovider.models["model-gone"]).toBeNull()
})
it("emits null sentinels for variants removed from a model that still exists", async () => {
it("emits null sentinels when reasoning and variants are removed from a model", async () => {
const existing = {
disabled_providers: [],
provider: {
@@ -270,11 +270,7 @@ describe("saveCustomProvider", () => {
name: "My Provider",
options: { baseURL: "https://example.com/v1" },
models: {
"model-1": {
name: "Model One",
reasoning: true,
variants: { high: { reasoningEffort: "high" } },
},
"model-1": { name: "Model One" },
},
}
await saveCustomProvider(ctx, "req", "myprovider", next, undefined, false, null, setCachedConfig)
@@ -283,11 +279,11 @@ describe("saveCustomProvider", () => {
const model = (
calls.config[0].config.provider as Record<
string,
{ models: Record<string, { variants?: Record<string, unknown> }> }
{ models: Record<string, { reasoning?: boolean | null; variants?: Record<string, unknown> }> }
>
).myprovider.models["model-1"]
expect(model.variants).toBeDefined()
expect(model.variants?.high).toBeDefined()
expect(model.reasoning).toBeNull()
expect(model.variants?.high).toBeNull()
expect(model.variants?.low).toBeNull()
})
+1 -1
View File
@@ -12,7 +12,7 @@ export const Model = Schema.Struct({
ai_sdk_provider: Schema.optional(Schema.Literals(AI_SDK_PROVIDERS)), // kilocode_change
release_date: Schema.optional(Schema.String),
attachment: Schema.optional(Schema.Boolean),
reasoning: Schema.optional(Schema.Boolean),
reasoning: Schema.optional(Schema.NullOr(Schema.Boolean)), // kilocode_change - allow null so reasoning can be removed via stripNulls on save
temperature: Schema.optional(Schema.Boolean),
tool_call: Schema.optional(Schema.Boolean),
interleaved: Schema.optional(
@@ -234,6 +234,61 @@ describe("kilocode indexing config", () => {
})
})
describe("custom provider model config", () => {
test("persists and removes reasoning across a global config reload", async () => {
await using globalTmp = await tmpdir()
const file = path.join(globalTmp.path, "kilo.json")
const prev = Global.Path.config
;(Global.Path as { config: string }).config = globalTmp.path
await clear()
await disposeAllInstances()
try {
await writeConfig(globalTmp.path, {
provider: {
custom: {
name: "Custom",
models: { model: { name: "Model" } },
},
},
})
await saveGlobal(
decode({
provider: {
custom: {
models: { model: { reasoning: true } },
},
},
}),
)
const added = JSON.parse(await Bun.file(file).text())
expect(added.provider.custom.models.model.reasoning).toBe(true)
await saveGlobal(
decode({
provider: {
custom: {
models: { model: { reasoning: null } },
},
},
}),
)
const written = JSON.parse(await Bun.file(file).text())
expect(written.provider.custom.models.model).not.toHaveProperty("reasoning")
await clear()
const reloaded = await Effect.runPromise(
Config.Service.use((svc) => svc.getGlobal()).pipe(Effect.scoped, Effect.provide(layer)),
)
expect(reloaded.provider?.custom?.models?.model?.reasoning).toBeUndefined()
} finally {
;(Global.Path as { config: string }).config = prev
await clear()
await disposeAllInstances()
}
})
})
describe("subagent variant overrides", () => {
test("removes one model override without removing sibling models", () => {
const patch = decode({