mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-30 17:14:40 +08:00
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:
@@ -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()
|
||||
})
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user