Merge pull request #9264 from Kilo-Org/cleanup/autocomplete-dead-code

refactor(vscode): remove dead code from autocomplete
This commit is contained in:
Mark IJbema
2026-04-21 10:48:24 +02:00
committed by GitHub
9 changed files with 22 additions and 475 deletions
@@ -4,18 +4,6 @@ import type { KiloConnectionService } from "../cli-backend"
const DEFAULT_MODEL = "mistralai/codestral-2508"
const PROVIDER_DISPLAY_NAME = "Kilo Gateway"
/** Chunk from an LLM streaming response */
export type ApiStreamChunk =
| { type: "text"; text: string }
| {
type: "usage"
totalCost?: number
inputTokens?: number
outputTokens?: number
cacheReadTokens?: number
cacheWriteTokens?: number
}
export class AutocompleteModel {
private connectionService: KiloConnectionService | null = null
public profileName: string | null = null
@@ -34,10 +22,6 @@ export class AutocompleteModel {
this.connectionService = service
}
public supportsFim(): boolean {
return true
}
/**
* Generate a FIM (Fill-in-the-Middle) completion via the CLI backend.
* Uses the SDK's kilo.fim() SSE endpoint which handles auth and streaming.
@@ -102,20 +86,6 @@ export class AutocompleteModel {
}
}
/**
* Generate response via chat completions (holefiller fallback).
* Not used when FIM is supported, but kept for compatibility.
*/
public async generateResponse(
systemPrompt: string,
userPrompt: string,
onChunk: (chunk: ApiStreamChunk) => void,
): Promise<ResponseMetaData> {
// FIM is the primary strategy; this method is a fallback.
// For now, throw — callers should use generateFimResponse via supportsFim().
throw new Error("Chat-based completions are not supported via CLI backend. Use FIM (supportsFim() returns true).")
}
public getModelName(): string {
return DEFAULT_MODEL
}
@@ -75,13 +75,6 @@ describe("AutocompleteModel", () => {
})
})
describe("supportsFim", () => {
it("always returns true", () => {
const model = new AutocompleteModel()
expect(model.supportsFim()).toBe(true)
})
})
describe("getModelName", () => {
it("returns the default model", () => {
const model = new AutocompleteModel()
@@ -165,13 +158,4 @@ describe("AutocompleteModel", () => {
)
})
})
describe("generateResponse", () => {
it("throws because FIM is the primary strategy", async () => {
const model = new AutocompleteModel()
await expect(model.generateResponse("system", "user", vi.fn())).rejects.toThrow(
"Chat-based completions are not supported via CLI backend",
)
})
})
})
@@ -98,22 +98,9 @@ export class ChatTextAreaAutocomplete {
let response = ""
try {
// Use FIM if supported, otherwise fall back to chat-based completion
if (this.model.supportsFim()) {
await this.model.generateFimResponse(prefix, suffix, (chunk) => {
response += chunk
})
} else {
// Fall back to chat-based completion for models without FIM support
const systemPrompt = this.getChatSystemPrompt()
const userPrompt = this.getChatUserPrompt(prefix)
await this.model.generateResponse(systemPrompt, userPrompt, (chunk) => {
if (chunk.type === "text") {
response += chunk.text
}
})
}
await this.model.generateFimResponse(prefix, suffix, (chunk) => {
response += chunk
})
const latencyMs = Date.now() - startTime
@@ -153,35 +140,6 @@ export class ChatTextAreaAutocomplete {
}
}
/**
* Get system prompt for chat-based completion
*/
private getChatSystemPrompt(): string {
return `You are an intelligent chat completion assistant. Your task is to complete the user's message naturally based on the provided context.
## RULES
- Provide a natural, conversational completion
- Be concise - typically 1-15 words
- Match the user's tone and style
- Use context from visible code if relevant
- NEVER repeat what the user already typed
- NEVER start with comments (//, /*, #)
- If the user is in the middle of typing a word (e.g., "hel"), include the COMPLETE word in your response (e.g., "hello world" not just "lo world")
- This allows proper prefix matching to remove the overlap correctly
- Return ONLY the completion text, no explanations or formatting`
}
/**
* Get user prompt for chat-based completion
*/
private getChatUserPrompt(prefix: string): string {
return `${prefix}
TASK: Complete the user's message naturally.
- If the user is mid-word (e.g., typed "hel"), return the COMPLETE word (e.g., "hello world") so prefix matching can work correctly
- Return ONLY the completion text (what comes next), no explanations.`
}
private async buildPrefix(userText: string, visibleCodeContext?: VisibleCodeContext): Promise<string> {
return buildChatPrefix(userText, visibleCodeContext?.editors)
}
@@ -1,84 +0,0 @@
import { ChatTextAreaAutocomplete } from "../ChatTextAreaAutocomplete"
import { ProviderSettingsManager } from "../../../../core/config/ProviderSettingsManager"
import { AutocompleteModel } from "../../AutocompleteModel"
import { ApiStreamChunk } from "../../../../api/transform/stream"
describe("ChatTextAreaAutocomplete", () => {
let autocomplete: ChatTextAreaAutocomplete
let mockProviderSettingsManager: ProviderSettingsManager
beforeEach(() => {
mockProviderSettingsManager = {} as ProviderSettingsManager
autocomplete = new ChatTextAreaAutocomplete(mockProviderSettingsManager)
})
describe("getCompletion", () => {
it("should work with non-FIM models using chat-based completion", async () => {
// Setup: Model without FIM support (like Mistral)
const mockModel = new AutocompleteModel()
vi.spyOn(mockModel, "hasValidCredentials").mockReturnValue(true)
vi.spyOn(mockModel, "supportsFim").mockReturnValue(false)
vi.spyOn(mockModel, "generateResponse").mockImplementation(async (systemPrompt, userPrompt, onChunk) => {
// Simulate streaming chat response
const chunks: ApiStreamChunk[] = [{ type: "text", text: "write a function" }]
for (const chunk of chunks) {
onChunk(chunk)
}
return {
cost: 0,
inputTokens: 15,
outputTokens: 8,
cacheWriteTokens: 0,
cacheReadTokens: 0,
}
})
// @ts-expect-error - accessing private property for test
autocomplete.model = mockModel
const result = await autocomplete.getCompletion("How to ")
expect(mockModel.generateResponse).toHaveBeenCalled()
expect(result.suggestion).toBe("write a function")
})
})
describe("cleanSuggestion", () => {
it("should filter code patterns (comments, preprocessor)", () => {
// Comments - filtered by the regex check in cleanSuggestion
expect(autocomplete.cleanSuggestion("// comment", "")).toBe("")
expect(autocomplete.cleanSuggestion("/* comment", "")).toBe("")
expect(autocomplete.cleanSuggestion("* something", "")).toBe("")
// Code patterns
expect(autocomplete.cleanSuggestion("#include", "")).toBe("")
expect(autocomplete.cleanSuggestion("# Header", "")).toBe("")
})
it("should filter empty content", () => {
// Empty content is filtered by postprocessAutocompleteSuggestion
expect(autocomplete.cleanSuggestion("", "")).toBe("")
})
it("should accept natural language suggestions", () => {
expect(autocomplete.cleanSuggestion("Hello world", "")).toBe("Hello world")
expect(autocomplete.cleanSuggestion("Can you help me", "")).toBe("Can you help me")
expect(autocomplete.cleanSuggestion("test123", "")).toBe("test123")
expect(autocomplete.cleanSuggestion("What's up?", "")).toBe("What's up?")
})
it("should accept symbols in middle of text", () => {
expect(autocomplete.cleanSuggestion("Text with # in middle", "")).toBe("Text with # in middle")
expect(autocomplete.cleanSuggestion("Hello // but not a comment", "")).toBe("Hello // but not a comment")
})
it("should truncate at first newline", () => {
expect(autocomplete.cleanSuggestion("First line\nSecond line", "")).toBe("First line")
})
it("should remove prefix overlap", () => {
expect(autocomplete.cleanSuggestion("Hello world", "Hello ")).toBe("world")
})
})
})
@@ -851,7 +851,6 @@ describe("AutocompleteInlineCompletionProvider", () => {
}),
getModelName: vi.fn().mockReturnValue("test-model"),
getProviderDisplayName: vi.fn().mockReturnValue("test-provider"),
supportsFim: vi.fn().mockReturnValue(true),
hasValidCredentials: vi.fn().mockReturnValue(true), // Default to true for tests
} as unknown as AutocompleteModel
mockCostTrackingCallback = vi.fn() as CostTrackingCallback
@@ -42,25 +42,15 @@ describe("postprocessAutocompleteSuggestion", () => {
expect(result).toBe("test")
})
it("handles Mercury/Granite prefix duplication", () => {
it("handles Mercury prefix duplication", () => {
const result = postprocessAutocompleteSuggestion({
suggestion: "const x = 42",
prefix: "const x = ",
suffix: "",
model: "granite-20b",
model: "inception/mercury-edit",
})
expect(result).toBe("42")
})
it("handles Gemini/Gemma file separator", () => {
const result = postprocessAutocompleteSuggestion({
suggestion: "const x = 1<|file_separator|>",
prefix: "",
suffix: "",
model: "gemini-pro",
})
expect(result).toBe("const x = 1")
})
})
describe("extreme repetition filtering", () => {
@@ -133,7 +133,7 @@ function normalizeToCompleteLine(params: AutocompleteSuggestion): AutocompleteSu
* @param params.suggestion - The suggested text to insert
* @param params.prefix - The text before the cursor position
* @param params.suffix - The text after the cursor position
* @param params.model - The model string (e.g., "codestral", "qwen3", etc.)
* @param params.model - The model string (e.g., "codestral", "mercury", etc.)
* @param params.languageId - Optional language ID for language-specific filtering
* @returns The processed suggestion text, or undefined if it should be filtered out
*/
@@ -133,34 +133,18 @@ export function postprocessCompletion({
}
}
if (llm.model.includes("qwen3")) {
// Qwen3 always starts from special thinking markers, and we don't want them to output these contents
// Remove all content from "
completion = completion.replace(/<think>.*?<\/think>/s, "")
completion = completion.replace(/<\/think>/, "")
// Remove any number of newline characters at the beginning and end
completion = completion.replace(/^\n+|\n+$/g, "")
}
if (llm.model.includes("mercury") || llm.model.includes("granite")) {
if (llm.model.includes("mercury")) {
completion = removePrefixOverlap(completion, prefix)
}
// // If completion starts with multiple whitespaces, but the cursor is at the end of the line
// // then it should probably be on a new line
if (
llm.model.includes("mercury") &&
(completion.startsWith(" ") || completion.startsWith("\t")) &&
!prefix.endsWith("\n") &&
(suffix.startsWith("\n") || suffix.trim().length === 0)
) {
completion = "\n" + completion
}
if ((llm.model.includes("gemini") || llm.model.includes("gemma")) && completion.endsWith("<|file_separator|>")) {
// "<|file_separator|>" is 18 characters long
completion = completion.slice(0, -18)
// If completion starts with multiple whitespaces, but the cursor is at the
// end of the line then it should probably be on a new line
if (
(completion.startsWith(" ") || completion.startsWith("\t")) &&
!prefix.endsWith("\n") &&
(suffix.startsWith("\n") || suffix.trim().length === 0)
) {
completion = "\n" + completion
}
}
// If prefix ends with space and so does completion, then remove the space from completion
@@ -1,8 +1,11 @@
// Fill in the middle prompts
//
// We only expose Codestral and Mercury Edit as autocomplete models — every
// other FIM template in the upstream continuedev list is unreachable.
import { CompletionOptions } from "../../index.js"
import { getLastNUriRelativePathParts, getShortestUniqueRelativeUriPaths } from "../../util/uri.js"
import { AutocompleteCodeSnippet, AutocompleteSnippet, AutocompleteSnippetType } from "../types.js"
import { AutocompleteSnippet, AutocompleteSnippetType } from "../types.js"
type TemplateRenderer = (
prefix: string,
@@ -27,55 +30,6 @@ export interface AutocompleteTemplate {
completionOptions?: Partial<CompletionOptions>
}
// https://huggingface.co/stabilityai/stable-code-3b
const stableCodeFimTemplate: AutocompleteTemplate = {
template: (prefix: string, suffix: string): string => {
return `<fim_prefix>${prefix}<fim_suffix>${suffix}<fim_middle>`
},
completionOptions: {
stop: ["<fim_prefix>", "<fim_suffix>", "<fim_middle>", "<file_sep>", "<|endoftext|>", "</fim_middle>", "</code>"],
},
}
// https://github.com/QwenLM/Qwen2.5-Coder?tab=readme-ov-file#3-file-level-code-completion-fill-in-the-middle
// This issue asks about the use of <|repo_name|> and <|file_sep|> together with <|fim_prefix|>, <|fim_suffix|> and <|fim_middle|>
// https://github.com/QwenLM/Qwen2.5-Coder/issues/343
const qwenCoderFimTemplate: AutocompleteTemplate = {
template: (prefix: string, suffix: string): string => {
return `<|fim_prefix|>${prefix}<|fim_suffix|>${suffix}<|fim_middle|>`
},
completionOptions: {
stop: [
"<|endoftext|>",
"<|fim_prefix|>",
"<|fim_middle|>",
"<|fim_suffix|>",
"<|fim_pad|>",
"<|repo_name|>",
"<|file_sep|>",
"<|im_start|>",
"<|im_end|>",
],
},
}
const seedCoderFimTemplate: AutocompleteTemplate = {
template: (prefix: string, suffix: string): string => {
return `<[fim-prefix]>${prefix}<[fim-suffix]>${suffix}<[fim-middle]>`
},
completionOptions: {
stop: [
"<[end▁of▁sentence]>",
"<[fim-prefix]>",
"<[fim-middle]>",
"<[fim-suffix]>",
"<[PAD▁TOKEN]>",
"<[SEP▁TOKEN]>",
"<[begin▁of▁sentence]>",
],
},
}
const codestralMultifileFimTemplate: AutocompleteTemplate = {
compilePrefixSuffix: (prefix, suffix, filepath, _reponame, snippets, workspaceUris): [string, string] => {
function getFileName(snippet: { uri: string; uniquePath: string }) {
@@ -158,217 +112,9 @@ const mercuryMultifileFimTemplate: AutocompleteTemplate = {
},
}
const codegemmaFimTemplate: AutocompleteTemplate = {
template: (prefix: string, suffix: string): string => {
return `<|fim_prefix|>${prefix}<|fim_suffix|>${suffix}<|fim_middle|>`
},
completionOptions: {
stop: ["<|fim_prefix|>", "<|fim_suffix|>", "<|fim_middle|>", "<|file_separator|>", "<end_of_turn>", "<eos>"],
},
}
const codeLlamaFimTemplate: AutocompleteTemplate = {
template: (prefix: string, suffix: string): string => {
return `<PRE> ${prefix} <SUF>${suffix} <MID>`
},
completionOptions: { stop: ["<PRE>", "<SUF>", "<MID>", "<EOT>"] },
}
// https://huggingface.co/deepseek-ai/deepseek-coder-1.3b-base
const deepseekFimTemplate: AutocompleteTemplate = {
template: (prefix: string, suffix: string): string => {
return `<fim▁begin>${prefix}<fim▁hole>${suffix}<fim▁end>`
},
completionOptions: {
stop: ["<fim▁begin>", "<fim▁hole>", "<fim▁end>", "//", "<end▁of▁sentence>"],
},
}
// https://github.com/THUDM/CodeGeeX4/blob/main/guides/Infilling_guideline.md
const codegeexFimTemplate: AutocompleteTemplate = {
template: (prefix, suffix, filepath, _reponame, language, allSnippets, workspaceUris): string => {
const snippets = allSnippets.filter(
(snippet) => snippet.type === AutocompleteSnippetType.Code,
) as AutocompleteCodeSnippet[]
const relativePaths = getShortestUniqueRelativeUriPaths(
[...snippets.map((snippet) => snippet.filepath), filepath],
workspaceUris,
)
const baseTemplate = `###PATH:${
relativePaths[relativePaths.length - 1]
}\n###LANGUAGE:${language}\n###MODE:BLOCK\n<|code_suffix|>${suffix}<|code_prefix|>${prefix}<|code_middle|>`
if (snippets.length === 0) {
return `<|user|>\n${baseTemplate}<|assistant|>\n`
}
const references = `###REFERENCE:\n${snippets
.map((snippet, i) => `###PATH:${relativePaths[i]}\n${snippet.content}\n`)
.join("###REFERENCE:\n")}`
const prompt = `<|user|>\n${references}\n${baseTemplate}<|assistant|>\n`
return prompt
},
completionOptions: {
stop: ["<|user|>", "<|code_suffix|>", "<|code_prefix|>", "<|code_middle|>", "<|assistant|>", "<|endoftext|>"],
},
}
const holeFillerTemplate: AutocompleteTemplate = {
template: (prefix: string, suffix: string) => {
// From https://github.com/VictorTaelin/AI-scripts
const SYSTEM_MSG = `You are a HOLE FILLER. You are provided with a file containing holes, formatted as '{{HOLE_NAME}}'. Your TASK is to complete with a string to replace this hole with, inside a <COMPLETION/> XML tag, including context-aware indentation, if needed. All completions MUST be truthful, accurate, well-written and correct.
## EXAMPLE QUERY:
<QUERY>
function sum_evens(lim) {
var sum = 0;
for (var i = 0; i < lim; ++i) {
{{FILL_HERE}}
}
return sum;
}
</QUERY>
TASK: Fill the {{FILL_HERE}} hole.
## CORRECT COMPLETION
<COMPLETION>if (i % 2 === 0) {
sum += i;
}</COMPLETION>
## EXAMPLE QUERY:
<QUERY>
def sum_list(lst):
total = 0
for x in lst:
{{FILL_HERE}}
return total
print sum_list([1, 2, 3])
</QUERY>
## CORRECT COMPLETION:
<COMPLETION> total += x</COMPLETION>
## EXAMPLE QUERY:
<QUERY>
// data Tree a = Node (Tree a) (Tree a) | Leaf a
// sum :: Tree Int -> Int
// sum (Node lft rgt) = sum lft + sum rgt
// sum (Leaf val) = val
// convert to TypeScript:
{{FILL_HERE}}
</QUERY>
## CORRECT COMPLETION:
<COMPLETION>type Tree<T>
= {$:"Node", lft: Tree<T>, rgt: Tree<T>}
| {$:"Leaf", val: T};
function sum(tree: Tree<number>): number {
switch (tree.$) {
case "Node":
return sum(tree.lft) + sum(tree.rgt);
case "Leaf":
return tree.val;
}
}</COMPLETION>
## EXAMPLE QUERY:
The 5th {{FILL_HERE}} is Jupiter.
## CORRECT COMPLETION:
<COMPLETION>planet from the Sun</COMPLETION>
## EXAMPLE QUERY:
function hypothenuse(a, b) {
return Math.sqrt({{FILL_HERE}}b ** 2);
}
## CORRECT COMPLETION:
<COMPLETION>a ** 2 + </COMPLETION>`
const fullPrompt =
SYSTEM_MSG +
`\n\n<QUERY>\n${prefix}{{FILL_HERE}}${suffix}\n</QUERY>\nTASK: Fill the {{FILL_HERE}} hole. Answer only with the CORRECT completion, and NOTHING ELSE. Do it now.\n<COMPLETION>`
return fullPrompt
},
completionOptions: {
stop: ["</COMPLETION>"],
},
}
export function getTemplateForModel(model: string): AutocompleteTemplate {
const lowerCaseModel = model.toLowerCase()
// if (lowerCaseModel.includes("starcoder2")) {
// return starcoder2FimTemplate;
// }
if (lowerCaseModel.includes("mercury")) {
if (model.toLowerCase().includes("mercury")) {
return mercuryMultifileFimTemplate
}
if (lowerCaseModel.includes("qwen") && lowerCaseModel.includes("coder")) {
return qwenCoderFimTemplate
}
if (lowerCaseModel.includes("seed") && lowerCaseModel.includes("coder")) {
return seedCoderFimTemplate
}
if (
lowerCaseModel.includes("starcoder") ||
lowerCaseModel.includes("star-coder") ||
lowerCaseModel.includes("starchat") ||
lowerCaseModel.includes("octocoder") ||
lowerCaseModel.includes("stable") ||
lowerCaseModel.includes("codeqwen") ||
lowerCaseModel.includes("qwen")
) {
return stableCodeFimTemplate
}
if (lowerCaseModel.includes("codestral")) {
return codestralMultifileFimTemplate
}
if (lowerCaseModel.includes("codegemma")) {
return codegemmaFimTemplate
}
if (lowerCaseModel.includes("codellama")) {
return codeLlamaFimTemplate
}
if (lowerCaseModel.includes("deepseek")) {
return deepseekFimTemplate
}
if (lowerCaseModel.includes("codegeex")) {
return codegeexFimTemplate
}
// if (
// lowerCaseModel.includes("gpt") ||
// lowerCaseModel.includes("davinci-002") ||
// lowerCaseModel.includes("claude") ||
// lowerCaseModel.includes("granite3") ||
// lowerCaseModel.includes("granite-3")
// ) {
// Default Fallback Mode
return holeFillerTemplate
//}
//return stableCodeFimTemplate
return codestralMultifileFimTemplate
}