Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
33eb706
fix(vscode): support direct autocomplete FIM providers
May 25, 2026
900b1fc
docs(vscode): clarify autocomplete FIM changeset
May 25, 2026
c09c5c4
refactor(vscode): route direct FIM through backend
May 25, 2026
d635a15
test(vscode): update autocomplete selector expectations
May 25, 2026
2bf49bd
refactor(gateway): extract FIM routing
May 25, 2026
25e89db
refactor(gateway): own autocomplete model catalog
May 25, 2026
afde386
fix(gateway): preserve legacy autocomplete model ids
May 25, 2026
4cd463d
refactor(gateway): split autocomplete provider and model
May 26, 2026
bfda8fe
fix(vscode): import browser-safe autocomplete catalog
May 26, 2026
08124be
Merge remote-tracking branch 'origin/main' into mark/direct-autocompl…
May 26, 2026
cc50d76
fix(kilo-docs): exclude FIM API endpoints from link check
May 26, 2026
cef6164
fix(vscode): use autocomplete provider for chat completions
May 26, 2026
19719ba
fix(gateway): use env keys for direct FIM providers
May 26, 2026
e5b2bb8
fix(gateway): preserve FIM gateway model passthrough
May 26, 2026
6fb50a8
fix(vscode): infer autocomplete provider from direct models
May 26, 2026
4ca226c
fix(gateway): cache working Mistral FIM endpoint
May 26, 2026
c073dac
refactor(gateway): clarify Mistral FIM endpoint helper
May 26, 2026
d02145a
refactor(gateway): simplify Mistral FIM endpoint cache
May 26, 2026
9b1407e
refactor(gateway): remove unshipped autocomplete aliases
May 26, 2026
5590b43
refactor(gateway): simplify autocomplete model lookup
May 26, 2026
762eb22
refactor(gateway): share autocomplete provider types
May 26, 2026
bbc57f6
Merge branch 'main' into mark/direct-autocomplete-fim-providers
May 26, 2026
923d955
fix: address autocomplete FIM review findings
May 26, 2026
80e39c2
Merge branch 'main' into mark/direct-autocomplete-fim-providers
May 26, 2026
be5b4f9
fix(vscode): never infer direct autocomplete provider from model name
May 26, 2026
80f8781
refactor(gateway): make FimTarget a discriminated union
May 26, 2026
21b5607
chore: trigger CI
May 26, 2026
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
5 changes: 5 additions & 0 deletions .changeset/direct-fim-providers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---

Add explicit Mistral and Inception autocomplete options that use connected provider API keys for FIM requests.
2 changes: 2 additions & 0 deletions packages/kilo-docs/lychee.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ exclude = [
'https://console.groq.com/pages/models',
'^https?://([A-Za-z0-9-]+\.)?glama\.ai(/|$)',
'^https?://(api|console)\.mistral\.ai',
'^https?://codestral\.mistral\.ai/v1/fim/completions/?$',
'^https?://api\.inceptionlabs\.ai/v1/fim/completions/?$',
'^https?://(app|console)\.requesty\.ai',
'^https?://(cloud|console)\.google\.ai',
# API endpoint prefixes are valid but return 4xx to plain GET link checks.
Expand Down
2 changes: 2 additions & 0 deletions packages/kilo-gateway/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
],
"exports": {
".": "./src/index.ts",
"./autocomplete": "./src/autocomplete.ts",
"./fim": "./src/fim.ts",
"./tui": "./src/tui.ts"
},
"files": [
Expand Down
101 changes: 101 additions & 0 deletions packages/kilo-gateway/src/autocomplete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
export type AutocompleteProviderID = "kilo" | "mistral" | "inception"
export type DirectAutocompleteProviderID = Exclude<AutocompleteProviderID, "kilo">

export interface AutocompleteModelDef {
/** Stable combined value for internal comparisons. */
readonly id: string
/** Model value stored in settings and sent to the FIM API. */
readonly modelID: string
/** Human-readable label shown in settings. */
readonly label: 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 upstream by the FIM route. */
readonly requestModel: string
/** Provider key to use for direct BYOK FIM. Empty means Kilo Gateway. */
readonly directProvider?: DirectAutocompleteProviderID
/** FIM request temperature. */
readonly temperature: number
}

const models: AutocompleteModelDef[] = [
{
id: "kilo/mistralai/codestral-2508",
modelID: "mistralai/codestral-2508",
label: "Codestral",
providerID: "kilo",
provider: "Kilo Gateway",
requestModel: "mistralai/codestral-2508",
temperature: 0.2,
},
{
id: "kilo/inception/mercury-edit-2",
modelID: "inception/mercury-edit-2",
label: "Mercury Edit 2",
providerID: "kilo",
provider: "Kilo Gateway",
requestModel: "inception/mercury-edit-2",
temperature: 0,
},
{
id: "mistral/codestral-2508",
modelID: "codestral-2508",
label: "Codestral",
providerID: "mistral",
provider: "Mistral",
requestModel: "codestral-2508",
directProvider: "mistral",
temperature: 0.2,
},
{
id: "inception/mercury-edit-2",
modelID: "mercury-edit-2",
label: "Mercury Edit 2",
providerID: "inception",
provider: "Inception",
requestModel: "mercury-edit-2",
directProvider: "inception",
temperature: 0,
},
]

export const AUTOCOMPLETE_MODELS: readonly AutocompleteModelDef[] = models

export const DEFAULT_AUTOCOMPLETE_MODEL: AutocompleteModelDef = models[0]!

const aliases: Record<string, string> = {
"inception/mercury-edit": "inception/mercury-edit-2",
}

export function getAutocompleteModel(provider?: string, model?: string): AutocompleteModelDef {
// When provider is unset, always default to Kilo Gateway. Direct-provider
// use must be opted into explicitly via the provider setting — never inferred
// from a model name, since the same plain model id can exist on multiple
// providers and we don't want to silently route legacy settings to BYOK.
const pid = provider ?? "kilo"
const mid = aliases[model ?? ""] ?? model
for (const m of models) {
if (m.providerID === pid && m.modelID === mid) return m
}
return DEFAULT_AUTOCOMPLETE_MODEL
}

export function getAutocompleteModelById(id: string): AutocompleteModelDef {
for (const m of models) {
if (m.id === id) 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)
}
34 changes: 34 additions & 0 deletions packages/kilo-gateway/src/fim.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { KILO_API_BASE } from "./api/constants.js"
import { getAutocompleteModel, type DirectAutocompleteProviderID } from "./autocomplete.js"

export { requestMistralFim } from "./mistral-fim-endpoint.js"

export const DIRECT_FIM_ENV: Record<DirectAutocompleteProviderID, string[]> = {
mistral: ["MISTRAL_API_KEY"],
inception: ["INCEPTION_API_KEY"],
}

export type FimTarget =
| { provider: "kilo"; model: string; url: string }
| { provider: "inception"; model: string; url: string }
| { provider: "mistral"; model: string }

const KILO_FIM_URL = KILO_API_BASE + "/api/fim/completions"
const INCEPTION_FIM_URL = "https://api.inceptionlabs.ai/v1/fim/completions"

function kiloTarget(model?: string): FimTarget {
return { provider: "kilo", model: model ?? "mistralai/codestral-2501", url: KILO_FIM_URL }
}

export function resolveFimTarget(provider?: string, model?: string): FimTarget {
if (!provider || provider === "kilo") return kiloTarget(model)

const info = getAutocompleteModel(provider, model)
if (info.directProvider === "mistral") {
return { provider: "mistral", model: info.requestModel }
}
if (info.directProvider === "inception") {
return { provider: "inception", model: info.requestModel, url: INCEPTION_FIM_URL }
}
return kiloTarget(model)
}
10 changes: 10 additions & 0 deletions packages/kilo-gateway/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,16 @@ export {
type KiloEmbeddingModelCatalog,
} from "./api/embedding-models.js"
export { resolveKiloGatewayBaseUrl, resolveKiloOpenRouterBaseUrl } from "./api/url.js"
export {
AUTOCOMPLETE_MODELS,
DEFAULT_AUTOCOMPLETE_MODEL,
getAutocompleteModel,
getAutocompleteModelById,
validAutocompleteModel,
validAutocompleteProvider,
type AutocompleteModelDef,
type AutocompleteProviderID,
} from "./autocomplete.js"
export {
fetchOrganizationModes,
clearModesCache,
Expand Down
30 changes: 30 additions & 0 deletions packages/kilo-gateway/src/mistral-fim-endpoint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
export const MISTRAL_FIM_URL = "https://api.mistral.ai/v1/fim/completions"
export const CODESTRAL_FIM_URL = "https://codestral.mistral.ai/v1/fim/completions"

let preferred: string | undefined

export function isMistralEndpointMismatch(response: Response) {
return response.status === 401 || response.status === 403
}

export function clearMistralFimEndpointCache() {
preferred = undefined
}

export function getCachedMistralFimEndpoint() {
return preferred
}

export async function requestMistralFim(request: (url: string) => Promise<Response>) {
const firstUrl = preferred ?? MISTRAL_FIM_URL
const secondUrl = firstUrl === MISTRAL_FIM_URL ? CODESTRAL_FIM_URL : MISTRAL_FIM_URL
const first = await request(firstUrl)

if (first.ok) return first
if (!isMistralEndpointMismatch(first)) return first

preferred = undefined
const second = await request(secondUrl)
if (second.ok) preferred = secondUrl
return second
}
120 changes: 120 additions & 0 deletions packages/kilo-gateway/src/server/fim.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { HEADER_FEATURE } from "../api/constants.js"
import type { DirectAutocompleteProviderID } from "../autocomplete.js"
import { DIRECT_FIM_ENV, requestMistralFim, resolveFimTarget, type FimTarget } from "../fim.js"
import { buildKiloHeaders } from "../headers.js"
import type { AuthStore } from "./handlers.js"

type Auth = Pick<AuthStore, "get">

const FIM_TIMEOUT_MS = 30_000

async function getProxyAuth(Auth: Auth) {
const auth = await Auth.get("kilo")
const token = auth?.type === "api" ? auth.key : auth?.type === "oauth" ? auth.access : undefined
return {
auth,
token,
organizationId: auth?.type === "oauth" ? auth.accountId : undefined,
}
}

async function getProviderKey(Auth: Auth, provider: DirectAutocompleteProviderID) {
const auth = await Auth.get(provider)
if (auth?.type === "api") return auth.key
return DIRECT_FIM_ENV[provider].map((key) => process.env[key]).find(Boolean)
}

async function fetchFim(
target: FimTarget,
key: string,
input: {
prefix: string
suffix: string
maxTokens: number
temperature: number
signal: AbortSignal
organizationId?: string
},
): Promise<Response> {
const run = async (url: string) => {
console.info(`[FIM] request provider=${target.provider} model=${target.model} url=${url}`)
return fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${key}`,
...(target.provider === "kilo"
? buildKiloHeaders(undefined, { kilocodeOrganizationId: input.organizationId })
: {}),
...(target.provider === "kilo" ? { [HEADER_FEATURE]: "autocomplete" } : {}),
},
signal: input.signal,
body: JSON.stringify({
model: target.model,
prompt: input.prefix,
suffix: input.suffix,
max_tokens: input.maxTokens,
temperature: input.temperature,
stream: true,
}),
})
}

if (target.provider === "mistral") return requestMistralFim(run)
return run(target.url)
}

export function createFimHandler(Auth: Auth) {
return async (c: any) => {
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
const token = target.provider === "kilo" ? proxy?.token : await getProviderKey(Auth, target.provider)

if (target.provider === "kilo" && !proxy?.auth) {
return c.json({ error: "Not authenticated with Kilo Gateway" }, 401)
}

if (target.provider === "kilo" && !token) {
return c.json({ error: "No valid token found" }, 401)
}

if (!token) {
return c.json({ error: `Missing ${target.provider} provider API key` }, 401)
}

const signal = AbortSignal.any([c.req.raw.signal, AbortSignal.timeout(FIM_TIMEOUT_MS)])

try {
const response = await fetchFim(target, token, {
prefix,
suffix,
maxTokens: fimMaxTokens,
temperature: fimTemperature,
signal,
organizationId: proxy?.organizationId,
})

if (!response.ok) {
const text = await response.text()
return c.json({ error: `FIM request failed: ${response.status} ${text}` }, response.status as any)
}

return new Response(response.body, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
})
} catch (err) {
if (err instanceof DOMException && err.name === "TimeoutError") {
return c.json({ error: "FIM request timed out" }, 504 as any)
}
if (signal.aborted) return c.json({ error: "FIM request canceled" }, 499 as any)
throw err
}
}
}
Loading
Loading