refactor(gateway): split autocomplete provider and model

This commit is contained in:
Mark IJbema
2026-05-26 09:15:42 +02:00
parent afde386730
commit 4cd463d619
19 changed files with 135 additions and 64 deletions
+38 -12
View File
@@ -1,15 +1,17 @@
export type AutocompleteProviderID = "kilo" | "mistral" | "inception"
export interface AutocompleteModelDef {
/** Stable setting value. */
/** Stable combined value for internal comparisons. */
readonly id: string
/** Model ID displayed under the selector provider group. */
/** Model value stored in settings and sent to the FIM API. */
readonly modelID: string
/** Human-readable label shown in settings. */
readonly label: string
/** Provider ID used by the selector group. */
readonly providerID: string
/** Provider value stored in settings and used by the selector group. */
readonly providerID: AutocompleteProviderID
/** Provider display name for status bar / telemetry. */
readonly provider: string
/** Full model ID sent to the FIM API. */
/** Full model ID sent upstream by the FIM route. */
readonly requestModel: string
/** Provider key to use for direct BYOK FIM. Empty means Kilo Gateway. */
readonly directProvider?: "mistral" | "inception"
@@ -47,7 +49,7 @@ const models: AutocompleteModelDef[] = [
temperature: 0.2,
},
{
id: "inception/provider/mercury-edit-2",
id: "inception/mercury-edit-2",
modelID: "mercury-edit-2",
label: "Mercury Edit 2",
providerID: "inception",
@@ -63,15 +65,39 @@ export const AUTOCOMPLETE_MODELS: readonly AutocompleteModelDef[] = models
export const DEFAULT_AUTOCOMPLETE_MODEL: AutocompleteModelDef = models[0]!
const aliases: Record<string, string> = {
"mistralai/codestral-2508": "kilo/mistralai/codestral-2508",
"inception/mercury-edit": "kilo/inception/mercury-edit-2",
"inception/mercury-edit-2": "kilo/inception/mercury-edit-2",
"kilo/mistralai/codestral-2508": "mistralai/codestral-2508",
"kilo/inception/mercury-edit-2": "inception/mercury-edit-2",
"inception/mercury-edit": "inception/mercury-edit-2",
}
export function getAutocompleteModel(id: string): AutocompleteModelDef {
const resolved = aliases[id] ?? id
export function getAutocompleteModel(provider?: string, model?: string): AutocompleteModelDef {
if (model === undefined) {
const id = provider ?? ""
for (const m of models) {
if (m.id === id) return m
}
const mid = aliases[id] ?? id
for (const m of models) {
if (m.providerID === "kilo" && m.modelID === mid) return m
}
return DEFAULT_AUTOCOMPLETE_MODEL
}
const pid = provider || "kilo"
const mid = aliases[model ?? ""] ?? model
for (const m of models) {
if (m.id === resolved) return m
if (m.providerID === pid && m.modelID === mid) return m
}
return DEFAULT_AUTOCOMPLETE_MODEL
}
export function validAutocompleteProvider(value: unknown) {
if (typeof value !== "string") return false
return models.some((m) => m.providerID === value)
}
export function validAutocompleteModel(value: unknown) {
if (typeof value !== "string") return false
const resolved = aliases[value] ?? value
return models.some((m) => m.modelID === resolved)
}
+3
View File
@@ -45,7 +45,10 @@ export {
AUTOCOMPLETE_MODELS,
DEFAULT_AUTOCOMPLETE_MODEL,
getAutocompleteModel,
validAutocompleteModel,
validAutocompleteProvider,
type AutocompleteModelDef,
type AutocompleteProviderID,
} from "./autocomplete.js"
export {
fetchOrganizationModes,
+7 -5
View File
@@ -18,8 +18,8 @@ const MISTRAL_FIM_URL = "https://api.mistral.ai/v1/fim/completions"
const CODESTRAL_FIM_URL = "https://codestral.mistral.ai/v1/fim/completions"
const INCEPTION_FIM_URL = "https://api.inceptionlabs.ai/v1/fim/completions"
export function resolveFimTarget(model?: string): FimTarget {
const info = getAutocompleteModel(model ?? "")
export function resolveFimTarget(provider?: string, model?: string): FimTarget {
const info = getAutocompleteModel(provider, model)
if (info.directProvider === "mistral") {
return { provider: "mistral", model: info.requestModel, urls: [MISTRAL_FIM_URL, CODESTRAL_FIM_URL] }
}
@@ -64,7 +64,9 @@ async function fetchFim(
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${key}`,
...(target.provider === "kilo" ? buildKiloHeaders(undefined, { kilocodeOrganizationId: input.organizationId }) : {}),
...(target.provider === "kilo"
? buildKiloHeaders(undefined, { kilocodeOrganizationId: input.organizationId })
: {}),
...(target.provider === "kilo" ? { [HEADER_FEATURE]: "autocomplete" } : {}),
},
signal: input.signal,
@@ -85,8 +87,8 @@ async function fetchFim(
export function createFimHandler(Auth: Auth) {
return async (c: any) => {
const { prefix, suffix, model, maxTokens, temperature } = c.req.valid("json")
const target = resolveFimTarget(model)
const { prefix, suffix, provider, model, maxTokens, temperature } = c.req.valid("json")
const target = resolveFimTarget(provider, model)
const fimMaxTokens = maxTokens ?? 256
const fimTemperature = temperature ?? 0.2
const proxy = target.provider === "kilo" ? await getProxyAuth(Auth) : undefined
@@ -332,6 +332,7 @@ export function createKiloRoutes(deps: KiloRoutesDeps) {
z.object({
prefix: z.string(),
suffix: z.string(),
provider: z.string().optional(),
model: z.string().optional(),
maxTokens: z.number().optional(),
temperature: z.number().optional(),
+7 -7
View File
@@ -3,12 +3,12 @@ import { resolveFimTarget } from "../src/server/fim"
describe("FIM target resolution", () => {
test("keeps gateway autocomplete models on Kilo Gateway", () => {
expect(resolveFimTarget("kilo/mistralai/codestral-2508")).toEqual({
expect(resolveFimTarget("kilo", "mistralai/codestral-2508")).toEqual({
provider: "kilo",
model: "mistralai/codestral-2508",
urls: ["https://api.kilo.ai/api/fim/completions"],
})
expect(resolveFimTarget("kilo/inception/mercury-edit-2")).toEqual({
expect(resolveFimTarget("kilo", "inception/mercury-edit-2")).toEqual({
provider: "kilo",
model: "inception/mercury-edit-2",
urls: ["https://api.kilo.ai/api/fim/completions"],
@@ -16,12 +16,12 @@ describe("FIM target resolution", () => {
})
test("routes explicit provider autocomplete models directly", () => {
expect(resolveFimTarget("mistral/codestral-2508")).toEqual({
expect(resolveFimTarget("mistral", "codestral-2508")).toEqual({
provider: "mistral",
model: "codestral-2508",
urls: ["https://api.mistral.ai/v1/fim/completions", "https://codestral.mistral.ai/v1/fim/completions"],
})
expect(resolveFimTarget("inception/provider/mercury-edit-2")).toEqual({
expect(resolveFimTarget("inception", "mercury-edit-2")).toEqual({
provider: "inception",
model: "mercury-edit-2",
urls: ["https://api.inceptionlabs.ai/v1/fim/completions"],
@@ -29,17 +29,17 @@ describe("FIM target resolution", () => {
})
test("maps legacy gateway IDs to explicit Kilo Gateway targets", () => {
expect(resolveFimTarget("mistralai/codestral-2508")).toEqual({
expect(resolveFimTarget(undefined, "mistralai/codestral-2508")).toEqual({
provider: "kilo",
model: "mistralai/codestral-2508",
urls: ["https://api.kilo.ai/api/fim/completions"],
})
expect(resolveFimTarget("inception/mercury-edit")).toEqual({
expect(resolveFimTarget(undefined, "inception/mercury-edit")).toEqual({
provider: "kilo",
model: "inception/mercury-edit-2",
urls: ["https://api.kilo.ai/api/fim/completions"],
})
expect(resolveFimTarget("inception/mercury-edit-2")).toEqual({
expect(resolveFimTarget(undefined, "inception/mercury-edit-2")).toEqual({
provider: "kilo",
model: "inception/mercury-edit-2",
urls: ["https://api.kilo.ai/api/fim/completions"],
+18 -4
View File
@@ -752,10 +752,10 @@
"kilo-code.new.autocomplete.model": {
"type": "string",
"enum": [
"kilo/mistralai/codestral-2508",
"kilo/inception/mercury-edit-2",
"mistral/codestral-2508",
"inception/provider/mercury-edit-2"
"mistralai/codestral-2508",
"inception/mercury-edit-2",
"codestral-2508",
"mercury-edit-2"
],
"enumDescriptions": [
"Codestral via Kilo Gateway (default)",
@@ -765,6 +765,20 @@
],
"description": "Model to use for inline autocomplete suggestions"
},
"kilo-code.new.autocomplete.provider": {
"type": "string",
"enum": [
"kilo",
"mistral",
"inception"
],
"enumDescriptions": [
"Use autocomplete models through Kilo Gateway",
"Use autocomplete models through your connected Mistral provider API key",
"Use autocomplete models through your connected Inception provider API key"
],
"description": "Provider to use for inline autocomplete suggestions. If unset, Kilo Gateway is used."
},
"kilo-code.new.autocomplete.enableAutoTrigger": {
"type": "boolean",
"default": true,
@@ -23,11 +23,13 @@ export interface AutocompleteServiceSettings {
function readSettings(): AutocompleteServiceSettings {
const config = vscode.workspace.getConfiguration(CONFIG_SECTION)
const info = getAutocompleteModel(config.get<string>("provider"), config.get<string>("model"))
return {
enableAutoTrigger: config.get<boolean>("enableAutoTrigger") ?? true,
enableSmartInlineTaskKeybinding: config.get<boolean>("enableSmartInlineTaskKeybinding") ?? true,
enableChatAutocomplete: config.get<boolean>("enableChatAutocomplete") ?? true,
model: getAutocompleteModel(config.get<string>("model") ?? "").id,
provider: info.providerID,
model: info.modelID,
snoozeUntil: config.get<number>("snoozeUntil"),
}
}
@@ -119,9 +121,7 @@ export class AutocompleteServiceManager {
public async load() {
this.settings = readSettings()
if (this.settings.model) {
this.inlineCompletionProvider.setModel(this.settings.model)
}
this.inlineCompletionProvider.setModel(getAutocompleteModel(this.settings.provider, this.settings.model).id)
await this.updateGlobalContext()
this.updateStatusBar()
@@ -319,11 +319,13 @@ export class AutocompleteServiceManager {
}
private getCurrentModelName(): string {
return getAutocompleteModel(this.inlineCompletionProvider.getModelId()).label
const info = getAutocompleteModel(this.settings?.provider, this.settings?.model)
return info.label
}
private getCurrentProviderName(): string {
return getAutocompleteModel(this.inlineCompletionProvider.getModelId()).provider
const info = getAutocompleteModel(this.settings?.provider, this.settings?.model)
return info.provider
}
private hasNoUsableProvider(): boolean {
@@ -25,43 +25,50 @@ describe("autocomplete settings", () => {
})
it("includes the configured direct provider model in loaded settings", async () => {
state.set("model", "inception/provider/mercury-edit-2")
state.set("provider", "inception")
state.set("model", "mercury-edit-2")
const { buildAutocompleteSettingsMessage } = await import("../settings")
expect(buildAutocompleteSettingsMessage().settings.model).toBe("inception/provider/mercury-edit-2")
expect(buildAutocompleteSettingsMessage().settings.provider).toBe("inception")
expect(buildAutocompleteSettingsMessage().settings.model).toBe("mercury-edit-2")
})
it("defaults to codestral when no model is set", async () => {
const { buildAutocompleteSettingsMessage } = await import("../settings")
expect(buildAutocompleteSettingsMessage().settings.model).toBe("kilo/mistralai/codestral-2508")
expect(buildAutocompleteSettingsMessage().settings.provider).toBe("kilo")
expect(buildAutocompleteSettingsMessage().settings.model).toBe("mistralai/codestral-2508")
})
it("defaults to codestral when stored model is no longer supported", async () => {
state.set("model", "some/removed-model")
const { buildAutocompleteSettingsMessage } = await import("../settings")
expect(buildAutocompleteSettingsMessage().settings.model).toBe("kilo/mistralai/codestral-2508")
expect(buildAutocompleteSettingsMessage().settings.provider).toBe("kilo")
expect(buildAutocompleteSettingsMessage().settings.model).toBe("mistralai/codestral-2508")
})
it("maps legacy inception/mercury-edit to Kilo Gateway Mercury", async () => {
state.set("model", "inception/mercury-edit")
const { buildAutocompleteSettingsMessage } = await import("../settings")
expect(buildAutocompleteSettingsMessage().settings.model).toBe("kilo/inception/mercury-edit-2")
expect(buildAutocompleteSettingsMessage().settings.provider).toBe("kilo")
expect(buildAutocompleteSettingsMessage().settings.model).toBe("inception/mercury-edit-2")
})
it("maps legacy inception/mercury-edit-2 to Kilo Gateway Mercury", async () => {
state.set("model", "inception/mercury-edit-2")
const { buildAutocompleteSettingsMessage } = await import("../settings")
expect(buildAutocompleteSettingsMessage().settings.model).toBe("kilo/inception/mercury-edit-2")
expect(buildAutocompleteSettingsMessage().settings.provider).toBe("kilo")
expect(buildAutocompleteSettingsMessage().settings.model).toBe("inception/mercury-edit-2")
})
it("validates supported model updates", async () => {
const { validAutocompleteSetting } = await import("../settings")
expect(validAutocompleteSetting("model", "inception/provider/mercury-edit-2")).toBe(true)
expect(validAutocompleteSetting("model", "mercury-edit-2")).toBe(true)
expect(validAutocompleteSetting("provider", "inception")).toBe(true)
})
it("rejects unsupported model updates", async () => {
@@ -35,7 +35,8 @@ export async function generateFim(
{
prefix,
suffix,
model: info.id,
provider: info.providerID,
model: info.modelID,
maxTokens: FIM_MAX_TOKENS,
temperature: info.temperature,
},
@@ -1,5 +1,10 @@
import * as vscode from "vscode"
import { AUTOCOMPLETE_MODELS, getAutocompleteModel } from "../../shared/autocomplete-models"
import {
DEFAULT_AUTOCOMPLETE_MODEL,
getAutocompleteModel,
validAutocompleteModel,
validAutocompleteProvider,
} from "../../shared/autocomplete-models"
type Message = {
type: string
@@ -18,13 +23,15 @@ export async function routeAutocompleteMessage(message: Message, post: Post): Pr
export function buildAutocompleteSettingsMessage() {
const config = vscode.workspace.getConfiguration("kilo-code.new.autocomplete")
const info = getAutocompleteModel(config.get<string>("provider"), config.get<string>("model"))
return {
type: "autocompleteSettingsLoaded" as const,
settings: {
enableAutoTrigger: config.get<boolean>("enableAutoTrigger", true),
enableSmartInlineTaskKeybinding: config.get<boolean>("enableSmartInlineTaskKeybinding", false),
enableChatAutocomplete: config.get<boolean>("enableChatAutocomplete", false),
model: getAutocompleteModel(config.get<string>("model") ?? "").id,
provider: info.providerID,
model: info.modelID,
},
}
}
@@ -40,8 +47,11 @@ export function watchAutocompleteConfig(post: Post): vscode.Disposable {
export function validAutocompleteSetting(key: string, value: unknown) {
if (key === "model") {
if (typeof value !== "string") return false
return AUTOCOMPLETE_MODELS.some((m) => m.id === value)
return validAutocompleteModel(value)
}
if (key === "provider") {
return validAutocompleteProvider(value ?? DEFAULT_AUTOCOMPLETE_MODEL.providerID)
}
if (key === "enableAutoTrigger") return typeof value === "boolean"
@@ -2,5 +2,8 @@ export {
AUTOCOMPLETE_MODELS,
DEFAULT_AUTOCOMPLETE_MODEL,
getAutocompleteModel,
validAutocompleteModel,
validAutocompleteProvider,
type AutocompleteModelDef,
type AutocompleteProviderID,
} from "@kilocode/kilo-gateway"
@@ -7,8 +7,8 @@ describe("autocomplete model enum ↔ AUTOCOMPLETE_MODELS sync", () => {
const pkg = JSON.parse(readFileSync(join(__dirname, "../../package.json"), "utf8"))
const prop = pkg.contributes.configuration.properties["kilo-code.new.autocomplete.model"]
it("package.json enum matches AUTOCOMPLETE_MODELS ids", () => {
const ids = AUTOCOMPLETE_MODELS.map((m) => m.id)
it("package.json enum matches AUTOCOMPLETE_MODELS model IDs", () => {
const ids = AUTOCOMPLETE_MODELS.map((m) => m.modelID)
expect(prop.enum).toEqual(ids)
})
@@ -7,18 +7,16 @@ import { parseModelString } from "../../../../src/shared/provider-model"
import { DEFAULT_AUTOCOMPLETE_MODEL } from "../../../../src/shared/autocomplete-models"
import { ModelSelectorBase } from "../shared/ModelSelector"
import SettingsRow from "./SettingsRow"
import {
AUTOCOMPLETE_SELECTOR_MODELS,
getAutocompleteSelection,
getAutocompleteSettingID,
} from "./autocomplete-model-selector"
import { AUTOCOMPLETE_SELECTOR_MODELS, getAutocompleteSelection } from "./autocomplete-model-selector"
const ModelsTab: Component = () => {
const { config, settings, updateConfig, updateSetting } = useConfig()
const language = useLanguage()
const session = useSession()
const autocompleteModel = () => String(settings()["autocomplete.model"] ?? DEFAULT_AUTOCOMPLETE_MODEL.id)
const autocompleteProvider = () =>
String(settings()["autocomplete.provider"] ?? DEFAULT_AUTOCOMPLETE_MODEL.providerID)
const autocompleteModel = () => String(settings()["autocomplete.model"] ?? DEFAULT_AUTOCOMPLETE_MODEL.modelID)
function handleModelSelect(configKey: "model" | "small_model") {
return (providerID: string, modelID: string) => {
@@ -43,9 +41,9 @@ const ModelsTab: Component = () => {
}
function handleAutocompleteModelSelect(providerID: string, modelID: string) {
const id = getAutocompleteSettingID(providerID, modelID)
if (!id) return
updateSetting("autocomplete.model", id)
if (!providerID || !modelID) return
updateSetting("autocomplete.provider", providerID)
updateSetting("autocomplete.model", modelID)
}
return (
@@ -82,7 +80,7 @@ const ModelsTab: Component = () => {
last
>
<ModelSelectorBase
value={getAutocompleteSelection(autocompleteModel())}
value={getAutocompleteSelection(autocompleteProvider(), autocompleteModel())}
onSelect={handleAutocompleteModelSelect}
placement="bottom-start"
models={AUTOCOMPLETE_SELECTOR_MODELS}
@@ -1,15 +1,11 @@
import { AUTOCOMPLETE_MODELS, getAutocompleteModel } from "../../../../src/shared/autocomplete-models"
import type { EnrichedModel } from "../../context/provider"
export function getAutocompleteSelection(id: string) {
const model = getAutocompleteModel(id)
export function getAutocompleteSelection(provider?: string, modelID?: string) {
const model = getAutocompleteModel(provider, modelID)
return { providerID: model.providerID, modelID: model.modelID }
}
export function getAutocompleteSettingID(providerID: string, modelID: string) {
return AUTOCOMPLETE_MODELS.find((m) => m.providerID === providerID && m.modelID === modelID)?.id
}
export const AUTOCOMPLETE_SELECTOR_MODELS: EnrichedModel[] = AUTOCOMPLETE_MODELS.map((m) => ({
id: m.modelID,
name: m.label,
@@ -76,6 +76,7 @@ export const ConfigProvider: ParentComponent = (props) => {
"autocomplete.enableAutoTrigger": message.settings.enableAutoTrigger,
"autocomplete.enableSmartInlineTaskKeybinding": message.settings.enableSmartInlineTaskKeybinding,
"autocomplete.enableChatAutocomplete": message.settings.enableChatAutocomplete,
"autocomplete.provider": message.settings.provider,
"autocomplete.model": message.settings.model,
})
return
@@ -329,6 +329,7 @@ export interface AutocompleteSettingsLoadedMessage {
enableAutoTrigger: boolean
enableSmartInlineTaskKeybinding: boolean
enableChatAutocomplete: boolean
provider: string
model: string
}
}
+2
View File
@@ -6017,6 +6017,7 @@ export class Kilo extends HeyApiClient {
workspace?: string
prefix?: string
suffix?: string
provider?: string
model?: string
maxTokens?: number
temperature?: number
@@ -6032,6 +6033,7 @@ export class Kilo extends HeyApiClient {
{ in: "query", key: "workspace" },
{ in: "body", key: "prefix" },
{ in: "body", key: "suffix" },
{ in: "body", key: "provider" },
{ in: "body", key: "model" },
{ in: "body", key: "maxTokens" },
{ in: "body", key: "temperature" },
+1
View File
@@ -7207,6 +7207,7 @@ export type KiloFimData = {
body?: {
prefix: string
suffix: string
provider?: string
model?: string
maxTokens?: number
temperature?: number
+3
View File
@@ -10458,6 +10458,9 @@
"suffix": {
"type": "string"
},
"provider": {
"type": "string"
},
"model": {
"type": "string"
},