Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit 66da314

Browse files
committed
feat: add FIM inline code completion support (Fill-in-the-Middle)
Implements a lightweight FIM completion provider that registers as a VS Code InlineCompletionItemProvider to provide ghost-text suggestions as the user types, similar to GitHub Copilot. Key components: - FimTokenFormatter: Maps model families to FIM token formats (DeepSeek, CodeLlama, StarCoder, Mistral/Codestral, Qwen, generic) - FimApiClient: Lightweight API client supporting /v1/completions, Ollama /api/generate, and Mistral /v1/fim/completions endpoints - FimCompletionProvider: VS Code InlineCompletionItemProvider with debouncing, caching, and cancellation support - FimService: Orchestrator managing provider lifecycle based on settings Settings added to GlobalSettings: - fimEnabled, fimProvider, fimModelId, fimBaseUrl, fimDebounceMs, fimMaxTokens, fimApiKey (secret) Supported providers: openai-compatible, deepseek, mistral, ollama Closes #12261
1 parent ad25634 commit 66da314

12 files changed

Lines changed: 1296 additions & 0 deletions

packages/types/src/global-settings.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,18 @@ export const globalSettingsSchema = z.object({
232232
* Tools in this list will be excluded from prompt generation and rejected at execution time.
233233
*/
234234
disabledTools: z.array(toolNamesSchema).optional(),
235+
236+
/**
237+
* FIM (Fill-in-the-Middle) inline code completion settings.
238+
* These are configured separately from the chat model to allow using
239+
* a cheap/fast FIM-specialized model (e.g., DeepSeek Coder, Codestral).
240+
*/
241+
fimEnabled: z.boolean().optional(),
242+
fimProvider: z.enum(["openai-compatible", "deepseek", "mistral", "ollama"]).optional(),
243+
fimModelId: z.string().optional(),
244+
fimBaseUrl: z.string().optional(),
245+
fimDebounceMs: z.number().min(0).optional(),
246+
fimMaxTokens: z.number().min(1).optional(),
235247
})
236248

237249
export type GlobalSettings = z.infer<typeof globalSettingsSchema>
@@ -285,6 +297,7 @@ export const SECRET_STATE_KEYS = [
285297
// Global secrets that are part of GlobalSettings (not ProviderSettings)
286298
export const GLOBAL_SECRET_KEYS = [
287299
"openRouterImageApiKey", // For image generation
300+
"fimApiKey", // For FIM inline code completion
288301
] as const
289302

290303
// Type for the actual secret storage keys

src/__mocks__/vscode.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ export const languages = {
9696
clear: () => {},
9797
dispose: () => {},
9898
}),
99+
registerInlineCompletionItemProvider: () => mockDisposable,
99100
}
100101

101102
export const extensions = {
@@ -152,6 +153,19 @@ export const CodeActionKind = {
152153

153154
export const EventEmitter = mockEventEmitter
154155

156+
export const InlineCompletionTriggerKind = {
157+
Invoke: 0,
158+
Automatic: 1,
159+
}
160+
161+
export const InlineCompletionItem = class {
162+
constructor(insertText, range, command) {
163+
this.insertText = insertText
164+
this.range = range
165+
this.command = command
166+
}
167+
}
168+
155169
export default {
156170
workspace,
157171
window,
@@ -171,4 +185,6 @@ export default {
171185
EventEmitter,
172186
CodeAction,
173187
CodeActionKind,
188+
InlineCompletionTriggerKind,
189+
InlineCompletionItem,
174190
}

src/extension.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import { openAiCodexOAuthManager } from "./integrations/openai-codex/oauth"
3636
import { McpServerManager } from "./services/mcp/McpServerManager"
3737
import { CodeIndexManager } from "./services/code-index/manager"
3838
import { MdmService } from "./services/mdm/MdmService"
39+
import { FimService } from "./services/fim"
3940
import { migrateSettings } from "./utils/migrateSettings"
4041
import { autoImportSettings } from "./utils/autoImportSettings"
4142
import { API } from "./extension/api"
@@ -168,6 +169,28 @@ export async function activate(context: vscode.ExtensionContext) {
168169

169170
const contextProxy = await ContextProxy.getInstance(context)
170171

172+
// Initialize FIM (Fill-in-the-Middle) inline completion service.
173+
const fimService = new FimService(outputChannel)
174+
context.subscriptions.push(fimService)
175+
176+
// Initialize FIM with current settings
177+
const initFimSettings = async () => {
178+
const globalSettings = contextProxy.getGlobalSettings()
179+
const fimApiKey = await context.secrets.get("fimApiKey")
180+
fimService.updateSettings(globalSettings, fimApiKey)
181+
}
182+
183+
void initFimSettings()
184+
185+
// Listen for secret storage changes to update FIM API key
186+
context.secrets.onDidChange(async (e: vscode.SecretStorageChangeEvent) => {
187+
if (e.key === "fimApiKey") {
188+
const globalSettings = contextProxy.getGlobalSettings()
189+
const fimApiKey = await context.secrets.get("fimApiKey")
190+
fimService.updateSettings(globalSettings, fimApiKey)
191+
}
192+
})
193+
171194
// Initialize code index managers for all workspace folders.
172195
const codeIndexManagers: CodeIndexManager[] = []
173196

src/services/fim/FimApiClient.ts

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
/**
2+
* Lightweight API client for FIM (Fill-in-the-Middle) completion requests.
3+
*
4+
* Supports the `/v1/completions` endpoint used by OpenAI-compatible servers,
5+
* DeepSeek, Ollama, and similar providers. This is the legacy completions
6+
* endpoint (not chat completions), which is better suited for raw FIM prompts.
7+
*/
8+
9+
import { formatFimPrompt } from "./FimTokenFormatter"
10+
11+
export interface FimRequestOptions {
12+
/** The FIM provider type */
13+
provider: "openai-compatible" | "deepseek" | "mistral" | "ollama"
14+
/** Base URL for the API endpoint */
15+
baseUrl: string
16+
/** API key for authentication */
17+
apiKey?: string
18+
/** Model ID to use */
19+
modelId: string
20+
/** Text before the cursor */
21+
prefix: string
22+
/** Text after the cursor */
23+
suffix: string
24+
/** Maximum tokens to generate */
25+
maxTokens: number
26+
/** Abort signal for cancellation */
27+
signal?: AbortSignal
28+
}
29+
30+
export interface FimResponse {
31+
/** The generated completion text */
32+
completion: string
33+
}
34+
35+
/**
36+
* Normalize a base URL by removing trailing slashes.
37+
*/
38+
function normalizeBaseUrl(url: string): string {
39+
return url.replace(/\/+$/, "")
40+
}
41+
42+
/**
43+
* Build the API endpoint URL based on the provider type.
44+
*/
45+
function buildEndpointUrl(provider: string, baseUrl: string): string {
46+
const normalized = normalizeBaseUrl(baseUrl)
47+
48+
switch (provider) {
49+
case "ollama":
50+
return `${normalized}/api/generate`
51+
case "mistral":
52+
return `${normalized}/v1/fim/completions`
53+
default:
54+
// openai-compatible and deepseek use /v1/completions
55+
return `${normalized}/v1/completions`
56+
}
57+
}
58+
59+
/**
60+
* Build the request body based on the provider type.
61+
*/
62+
function buildRequestBody(options: FimRequestOptions): Record<string, unknown> {
63+
const { provider, modelId, prefix, suffix, maxTokens } = options
64+
65+
switch (provider) {
66+
case "ollama":
67+
return {
68+
model: modelId,
69+
prompt: prefix,
70+
suffix: suffix,
71+
stream: false,
72+
options: {
73+
num_predict: maxTokens,
74+
temperature: 0.2,
75+
top_p: 0.9,
76+
},
77+
}
78+
case "mistral":
79+
return {
80+
model: modelId,
81+
prompt: prefix,
82+
suffix: suffix,
83+
max_tokens: maxTokens,
84+
temperature: 0.2,
85+
top_p: 0.9,
86+
stop: ["\n\n"],
87+
}
88+
default: {
89+
// openai-compatible and deepseek: format the FIM prompt with special tokens
90+
const prompt = formatFimPrompt(modelId, prefix, suffix)
91+
return {
92+
model: modelId,
93+
prompt,
94+
max_tokens: maxTokens,
95+
temperature: 0.2,
96+
top_p: 0.9,
97+
stop: ["\n\n", "<|fim", "<fim_", "[/MIDDLE]"],
98+
}
99+
}
100+
}
101+
}
102+
103+
/**
104+
* Extract the completion text from the provider response.
105+
*/
106+
function extractCompletion(provider: string, data: Record<string, unknown>): string {
107+
switch (provider) {
108+
case "ollama": {
109+
return (data.response as string) ?? ""
110+
}
111+
default: {
112+
// OpenAI-compatible response format
113+
const choices = data.choices as Array<{ text?: string; message?: { content?: string } }> | undefined
114+
if (!choices || choices.length === 0) {
115+
return ""
116+
}
117+
return choices[0].text ?? choices[0].message?.content ?? ""
118+
}
119+
}
120+
}
121+
122+
/**
123+
* Send a FIM completion request to the configured provider.
124+
*/
125+
export async function requestFimCompletion(options: FimRequestOptions): Promise<FimResponse> {
126+
const url = buildEndpointUrl(options.provider, options.baseUrl)
127+
const body = buildRequestBody(options)
128+
129+
const headers: Record<string, string> = {
130+
"Content-Type": "application/json",
131+
}
132+
133+
if (options.apiKey) {
134+
headers["Authorization"] = `Bearer ${options.apiKey}`
135+
}
136+
137+
const response = await fetch(url, {
138+
method: "POST",
139+
headers,
140+
body: JSON.stringify(body),
141+
signal: options.signal,
142+
})
143+
144+
if (!response.ok) {
145+
const errorText = await response.text().catch(() => "Unknown error")
146+
throw new Error(`FIM API request failed (${response.status}): ${errorText}`)
147+
}
148+
149+
const data = (await response.json()) as Record<string, unknown>
150+
const completion = extractCompletion(options.provider, data)
151+
152+
return { completion }
153+
}

0 commit comments

Comments
 (0)