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/broad-custom-provider-efforts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@kilocode/cli": patch
"kilo-code": patch
---

Automatically expose broad reasoning effort options for custom provider models and link saved providers to advanced JSON configuration.
9 changes: 1 addition & 8 deletions packages/kilo-vscode/src/shared/custom-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,7 @@ export const EnvSchema = z
.trim()
.regex(/^[A-Z_][A-Z0-9_]*$/, INVALID_ENV)

const VariantConfigSchema = z.object({
enable_thinking: z.boolean().optional(),
thinking: z.object({ type: z.enum(["enabled", "disabled", "adaptive"]) }).optional(),
reasoning_split: z.boolean().optional(),
reasoningEffort: z.enum(["none", "minimal", "low", "medium", "high", "xhigh"]).optional(),
effort: z.enum(["low", "medium", "high", "xhigh", "max"]).optional(),
chat_template_args: z.object({ enable_thinking: z.boolean() }).optional(),
})
const VariantConfigSchema = z.record(z.string(), z.unknown())

export type VariantConfig = z.infer<typeof VariantConfigSchema>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ describe("validateCustomProvider – variant name validation", () => {
]
const out = validateCustomProvider(args(form))
expect(out.result).toBeUndefined()
expect(out.errors.models[0].variants?.[0]?.name).toBe("provider.custom.error.required")
expect(out.errors.models[0].variants?.[0]?.name).toBe('variants[""]: provider.custom.error.required')
})

it("blocks submit and reports error when reasoning is enabled with a whitespace-only variant name", () => {
Expand All @@ -112,7 +112,7 @@ describe("validateCustomProvider – variant name validation", () => {
]
const out = validateCustomProvider(args(form))
expect(out.result).toBeUndefined()
expect(out.errors.models[0].variants?.[0]?.name).toBe("provider.custom.error.required")
expect(out.errors.models[0].variants?.[0]?.name).toBe('variants[" "]: provider.custom.error.required')
})

it("blocks submit and reports duplicate error for two variants with the same name", () => {
Expand Down Expand Up @@ -140,7 +140,7 @@ describe("validateCustomProvider – variant name validation", () => {
]
const out = validateCustomProvider(args(form))
expect(out.result).toBeUndefined()
expect(out.errors.models[0].variants?.[1]?.name).toBe("provider.custom.error.duplicate")
expect(out.errors.models[0].variants?.[1]?.name).toBe('variants["fast"]: provider.custom.error.duplicate')
})

it("ignores variants entirely when reasoning is disabled, even if they have empty names", () => {
Expand Down Expand Up @@ -206,6 +206,34 @@ describe("validateCustomProvider – variant name validation", () => {
})
})

it("preserves opaque variant options after the editor controls are removed", () => {
const form = base()
const raw = {
thinking: { type: "adaptive", display: "summarized" },
reasoningSummary: "auto",
include: ["reasoning.encrypted_content"],
customOption: { enabled: true },
}
form.models[0].reasoning = true
form.models[0].variants = [
{
name: "high",
raw,
enableThinking: undefined,
thinking: "adaptive",
splitReasoning: undefined,
outputEffort: undefined,
reasoningEffort: undefined,
chatTemplateArgs: undefined,
},
]

const out = validateCustomProvider(args(form))
expect(out.result).toBeDefined()
const saved = out.result!.config.models["model-1"] as Record<string, unknown>
expect(saved.variants).toEqual({ high: raw })
})

it("serializes image modality when supportsImages is set", () => {
const form = base()
form.models[0].supportsImages = true
Expand Down
29 changes: 29 additions & 0 deletions packages/kilo-vscode/tests/unit/custom-provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,35 @@ describe("sanitizeCustomProviderConfig", () => {
})
})

it("preserves opaque options on existing variants", () => {
const variant = {
thinking: { type: "adaptive", display: "summarized" },
reasoningEffort: "max",
reasoningSummary: "auto",
include: ["reasoning.encrypted_content"],
customOption: { enabled: true },
}
const result = sanitizeCustomProviderConfig({
name: "Thinking Provider",
options: { baseURL: "https://example.com/v1" },
models: {
"model-1": {
name: "Model One",
variants: { high: variant },
},
},
})

expect(result).toEqual({
value: {
npm: "@ai-sdk/openai-compatible",
name: "Thinking Provider",
options: { baseURL: "https://example.com/v1" },
models: { "model-1": { name: "Model One", variants: { high: variant } } },
},
})
})

it("preserves core custom model modalities", () => {
const result = sanitizeCustomProviderConfig({
name: "Media Provider",
Expand Down
14 changes: 14 additions & 0 deletions packages/kilo-vscode/tests/unit/open-config-message.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { describe, expect, it } from "bun:test"
import { configMessage } from "../../webview-ui/src/utils/open-config"

describe("configMessage", () => {
it("builds a global config request with localized labels", () => {
const message = configMessage("global", (key, params) => `${key}:${params?.scope ?? ""}`)

expect(message.type).toBe("openConfigFile")
expect(message.scope).toBe("global")
expect(message.labels.scope).toBe("settings.config.scope.global:")
expect(message.labels.title).toBe("settings.config.title:settings.config.scope.global:")
expect(message.labels.openFailed).toBe("settings.config.openFailed:settings.config.scope.global:")
})
})
21 changes: 21 additions & 0 deletions packages/kilo-vscode/tests/unit/provider-actions-save.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,27 @@ describe("saveCustomProvider", () => {
expect(calls.set).toEqual([{ providerID: "myprovider", auth: { type: "api", key: "sk-test" } }])
})

it("preserves opaque existing variant options through the save boundary", async () => {
const variant = {
thinking: { type: "adaptive", display: "summarized" },
reasoningEffort: "custom",
reasoningSummary: "auto",
include: ["reasoning.encrypted_content"],
customOption: { enabled: true },
}
const saved = {
...createSavedProvider(),
models: { "model-1": { name: "Model One", reasoning: true, variants: { high: variant } } },
}
const existing = { disabled_providers: [], provider: { myprovider: saved } }
const { ctx, calls, setCachedConfig } = createCtx(existing)

await saveCustomProvider(ctx, "req", "myprovider", saved, undefined, false, null, setCachedConfig)

const provider = (calls.config[0]?.config.provider as Record<string, typeof saved>).myprovider
expect(provider.models["model-1"].variants.high).toEqual(variant)
})

// Regression tests for https://github.com/Kilo-Org/kilocode/issues/9186
//
// The CLI's config.update endpoint deep-merges its payload with the existing
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { useProvider } from "../../context/provider"
import { useVSCode } from "../../context/vscode"
import type { ExtensionMessage, ProviderAuthState, ProviderConfig } from "../../types/messages"
import { createProviderAction } from "../../utils/provider-action"
import { configMessage } from "../../utils/open-config"
import { MASKED_CUSTOM_PROVIDER_KEY, resolveCustomProviderKey } from "../../../../src/shared/custom-provider"
import {
CUSTOM_PROVIDER_PACKAGE,
Expand Down Expand Up @@ -89,6 +90,7 @@ function modes(raw: unknown): Modalities {
function parseVariant([name, cfg]: [string, Record<string, unknown>]): VariantEntry {
return {
name,
raw: cfg,
enableThinking: typeof cfg.enable_thinking === "boolean" ? cfg.enable_thinking : undefined,
thinking:
typeof cfg.thinking === "object" && cfg.thinking !== null
Expand Down Expand Up @@ -460,25 +462,6 @@ const CustomProviderDialog = (props: CustomProviderDialogProps) => {
setErrors("headers", (v) => v.filter((_, i) => i !== index))
}

function addVariant(mi: number) {
const blank: VariantEntry = {
name: "",
enableThinking: undefined,
thinking: undefined,
splitReasoning: undefined,
reasoningEffort: undefined,
outputEffort: undefined,
chatTemplateArgs: undefined,
}
setForm("models", mi, "variants", (v) => [...v, blank])
setErrors("models", mi, "variants", (v) => [...(v ?? []), {}])
}

function removeVariant(mi: number, vi: number) {
setForm("models", mi, "variants", (v) => v.filter((_, i) => i !== vi))
setErrors("models", mi, "variants", (v) => (v ?? []).filter((_, i) => i !== vi))
}

function validate() {
const output = validateCustomProvider({
form,
Expand Down Expand Up @@ -581,6 +564,19 @@ const CustomProviderDialog = (props: CustomProviderDialogProps) => {
{language.t("provider.custom.description.link")}
</a>
{language.t("provider.custom.description.suffix")}
<Show when={editing()}>
<div style={{ "margin-top": "8px" }}>
<a
href="#"
onClick={(e) => {
e.preventDefault()
vscode.postMessage(configMessage("global", language.t))
}}
>
{language.t("provider.custom.edit.advanced")}
</a>
</div>
</Show>
</div>

<div style={{ display: "flex", "flex-direction": "column", gap: "16px" }}>
Expand Down Expand Up @@ -673,7 +669,6 @@ const CustomProviderDialog = (props: CustomProviderDialogProps) => {
{(m, i) => (
<ModelCard
m={m}
i={i}
errors={errors.models[i()] ?? {}}
t={language.t}
canRemove={form.models.length > 1}
Expand All @@ -682,23 +677,6 @@ const CustomProviderDialog = (props: CustomProviderDialogProps) => {
onChangeReasoning={(v) => setForm("models", i(), "reasoning", v)}
onChangeSupportsImages={(v) => setForm("models", i(), "supportsImages", v)}
onRemove={() => removeModel(i())}
onAddVariant={() => addVariant(i())}
onRemoveVariant={(vi) => removeVariant(i(), vi)}
onChangeVariantName={(vi, val) => setForm("models", i(), "variants", vi, "name", val)}
onChangeVariantEnableThinking={(vi, val) =>
setForm("models", i(), "variants", vi, "enableThinking", val)
}
onChangeVariantThinking={(vi, val) => setForm("models", i(), "variants", vi, "thinking", val)}
onChangeVariantSplitReasoning={(vi, val) =>
setForm("models", i(), "variants", vi, "splitReasoning", val)
}
onChangeVariantReasoningEffort={(vi, val) =>
setForm("models", i(), "variants", vi, "reasoningEffort", val)
}
onChangeVariantOutputEffort={(vi, val) => setForm("models", i(), "variants", vi, "outputEffort", val)}
onChangeVariantChatTemplateArgs={(vi, val) =>
setForm("models", i(), "variants", vi, "chatTemplateArgs", val)
}
/>
)}
</For>
Expand Down
Loading
Loading