Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
b8daa75
also show in the settings when no provider is available
markijbema Oct 10, 2025
e0000d3
deduplicate
markijbema Oct 10, 2025
b2e4811
remove unused import
markijbema Oct 10, 2025
1c6a416
remove unused param
markijbema Oct 10, 2025
651d87a
move model options together
markijbema Oct 10, 2025
68526fe
typesafe
markijbema Oct 10, 2025
6a76b40
simplify
markijbema Oct 10, 2025
f1cfd02
fix
markijbema Oct 14, 2025
20138b5
add missing translations
markijbema Oct 15, 2025
25d5e86
fix
markijbema Oct 15, 2025
7f5120c
simplify
markijbema Oct 15, 2025
3bbafa6
make provider selection code simpler
markijbema Oct 15, 2025
97d97f6
remove hardcoding
markijbema Oct 15, 2025
32a2de0
switch over which list we are looping
markijbema Oct 15, 2025
5b0e9b2
simplify
markijbema Oct 15, 2025
3194961
show provider name in autocomplete tooltip
markijbema Oct 15, 2025
d3e6337
remove hardcoded list
markijbema Oct 15, 2025
567869f
do not say we have loaded when we havent
markijbema Oct 15, 2025
e1f945b
add translation
markijbema Oct 15, 2025
09444a1
fix test
markijbema Oct 15, 2025
f20b4e3
inline
markijbema Oct 15, 2025
10c151a
extract method
markijbema Oct 15, 2025
853ae36
request live
markijbema Oct 15, 2025
c0423f7
add comment and make clear why we need a separate loaded var
markijbema Oct 15, 2025
2bdf686
remove commented out code
markijbema Oct 15, 2025
79d6c60
add provider and model to the settings
markijbema Oct 16, 2025
b353723
skip kilocode provider with 0 balance
markijbema Oct 15, 2025
f95f8ba
fix
markijbema Oct 16, 2025
f78f4d4
reuse balanche checking
markijbema Oct 16, 2025
47f5b42
remove translation
markijbema Oct 16, 2025
83aa99a
remove changes from settings screen
markijbema Oct 16, 2025
7e08560
also merge files here
markijbema Oct 16, 2025
d45e447
fix import
markijbema Oct 16, 2025
e68ea25
add mocks
markijbema Oct 16, 2025
6891691
remove redundant comments
markijbema Oct 16, 2025
3c50e8f
remove
markijbema Oct 16, 2025
17a2e12
update translations
markijbema Oct 16, 2025
21ab677
more translations
markijbema Oct 16, 2025
9c83b5d
fix
markijbema Oct 16, 2025
c626bdc
reload autocomplete after changing settings
markijbema Oct 17, 2025
a41b712
fix bug in balance check
markijbema Oct 17, 2025
9e3e2d4
less complex types
markijbema Oct 17, 2025
a47d6ca
refactor(types): relocate autocomplete provider config to kilocode
markijbema Oct 17, 2025
4dd1cd1
add org support
markijbema Oct 17, 2025
8776697
mark kilocode change
markijbema Oct 17, 2025
c5711a1
fix
markijbema Oct 17, 2025
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
116 changes: 114 additions & 2 deletions packages/types/src/__tests__/kilocode.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// npx vitest run src/__tests__/kilocode.test.ts

import { describe, it, expect } from "vitest"
import { ghostServiceSettingsSchema } from "../kilocode/kilocode.js"
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
import { ghostServiceSettingsSchema, checkKilocodeBalance } from "../kilocode/kilocode.js"

describe("ghostServiceSettingsSchema", () => {
describe("autoTriggerDelay", () => {
Expand Down Expand Up @@ -93,3 +93,115 @@ describe("ghostServiceSettingsSchema", () => {
})
})
})

describe("checkKilocodeBalance", () => {
const mockToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbnYiOiJwcm9kdWN0aW9uIn0.test"
const mockOrgId = "org-123"

beforeEach(() => {
global.fetch = vi.fn()
})

afterEach(() => {
vi.restoreAllMocks()
})

it("should return true when balance is positive", async () => {
vi.mocked(global.fetch).mockResolvedValueOnce({
ok: true,
json: async () => ({ balance: 100 }),
} as Response)

const result = await checkKilocodeBalance(mockToken)
expect(result).toBe(true)
expect(global.fetch).toHaveBeenCalledWith(
"https://api.kilocode.ai/api/profile/balance",
expect.objectContaining({
headers: expect.objectContaining({
Authorization: `Bearer ${mockToken}`,
}),
}),
)
})

it("should return false when balance is zero", async () => {
vi.mocked(global.fetch).mockResolvedValueOnce({
ok: true,
json: async () => ({ balance: 0 }),
} as Response)

const result = await checkKilocodeBalance(mockToken)
expect(result).toBe(false)
})

it("should return false when balance is negative", async () => {
vi.mocked(global.fetch).mockResolvedValueOnce({
ok: true,
json: async () => ({ balance: -10 }),
} as Response)

const result = await checkKilocodeBalance(mockToken)
expect(result).toBe(false)
})

it("should include organization ID in headers when provided", async () => {
vi.mocked(global.fetch).mockResolvedValueOnce({
ok: true,
json: async () => ({ balance: 100 }),
} as Response)

const result = await checkKilocodeBalance(mockToken, mockOrgId)
expect(result).toBe(true)
expect(global.fetch).toHaveBeenCalledWith(
"https://api.kilocode.ai/api/profile/balance",
expect.objectContaining({
headers: expect.objectContaining({
Authorization: `Bearer ${mockToken}`,
"X-KiloCode-OrganizationId": mockOrgId,
}),
}),
)
})

it("should not include organization ID in headers when not provided", async () => {
vi.mocked(global.fetch).mockResolvedValueOnce({
ok: true,
json: async () => ({ balance: 100 }),
} as Response)

await checkKilocodeBalance(mockToken)

const fetchCall = vi.mocked(global.fetch).mock.calls[0]
expect(fetchCall).toBeDefined()
const headers = (fetchCall![1] as RequestInit)?.headers as Record<string, string>

expect(headers).toHaveProperty("Authorization")
expect(headers).not.toHaveProperty("X-KiloCode-OrganizationId")
})

it("should return false when API request fails", async () => {
vi.mocked(global.fetch).mockResolvedValueOnce({
ok: false,
} as Response)

const result = await checkKilocodeBalance(mockToken)
expect(result).toBe(false)
})

it("should return false when fetch throws an error", async () => {
vi.mocked(global.fetch).mockRejectedValueOnce(new Error("Network error"))

const result = await checkKilocodeBalance(mockToken)
expect(result).toBe(false)
})

it("should handle missing balance field in response", async () => {
vi.mocked(global.fetch).mockResolvedValueOnce({
ok: true,
json: async () => ({}),
} as Response)

const result = await checkKilocodeBalance(mockToken)
expect(result).toBe(false)
})
})
84 changes: 84 additions & 0 deletions packages/types/src/kilocode/kilocode.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { z } from "zod"
import { ProviderSettings, ProviderSettingsEntry } from "../provider-settings.js"

export const ghostServiceSettingsSchema = z
.object({
Expand All @@ -7,6 +8,8 @@ export const ghostServiceSettingsSchema = z
enableQuickInlineTaskKeybinding: z.boolean().optional(),
enableSmartInlineTaskKeybinding: z.boolean().optional(),
showGutterAnimation: z.boolean().optional(),
provider: z.string().optional(),
model: z.string().optional(),
})
.optional()

Expand Down Expand Up @@ -52,3 +55,84 @@ export function getKiloBaseUriFromToken(kilocodeToken?: string) {
}
return "https://api.kilocode.ai"
}

/**
* Check if the Kilocode account has a positive balance
* @param kilocodeToken - The Kilocode JWT token
* @param kilocodeOrganizationId - Optional organization ID to include in headers
* @returns Promise<boolean> - True if balance > 0, false otherwise
*/
export async function checkKilocodeBalance(kilocodeToken: string, kilocodeOrganizationId?: string): Promise<boolean> {
try {
const baseUrl = getKiloBaseUriFromToken(kilocodeToken)

const headers: Record<string, string> = {
Authorization: `Bearer ${kilocodeToken}`,
}

if (kilocodeOrganizationId) {
headers["X-KiloCode-OrganizationId"] = kilocodeOrganizationId
}

const response = await fetch(`${baseUrl}/api/profile/balance`, {
headers,
})

if (!response.ok) {
return false
}

const data = await response.json()
const balance = data.balance ?? 0
Copy link

Copilot AI Oct 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] API parsing assumes a top-level balance; tests currently mock nested { data: { balance } }. If the real endpoint returns nested balance, this logic will incorrectly treat it as zero. Confirm endpoint shape and, if nested, change to const balance = (data.balance ?? data.data?.balance ?? 0).

Suggested change
const balance = data.balance ?? 0
const balance = data.balance ?? data.data?.balance ?? 0

Copilot uses AI. Check for mistakes.

return balance > 0
} catch (error) {
console.error("Error checking kilocode balance:", error)
return false
}
}

export const AUTOCOMPLETE_PROVIDER_MODELS = {
mistral: "codestral-latest",
kilocode: "mistralai/codestral-2508",
openrouter: "mistralai/codestral-2508",
} as const
export type AutocompleteProviderKey = keyof typeof AUTOCOMPLETE_PROVIDER_MODELS

interface ProviderSettingsManager {
listConfig(): Promise<ProviderSettingsEntry[]>
getProfile(params: { id: string }): Promise<ProviderSettings>
}

export type ProviderUsabilityChecker = (
provider: AutocompleteProviderKey,
providerSettingsManager: ProviderSettingsManager,
) => Promise<boolean>

export const defaultProviderUsabilityChecker: ProviderUsabilityChecker = async (provider, providerSettingsManager) => {
if (provider === "kilocode") {
try {
const profiles = await providerSettingsManager.listConfig()
const kilocodeProfile = profiles.find((p) => p.apiProvider === "kilocode")

if (!kilocodeProfile) {
return false
}

const profile = await providerSettingsManager.getProfile({ id: kilocodeProfile.id })
const kilocodeToken = profile.kilocodeToken
const kilocodeOrgId = profile.kilocodeOrganizationId

if (!kilocodeToken) {
return false
}

return await checkKilocodeBalance(kilocodeToken, kilocodeOrgId)
} catch (error) {
console.error("Error checking kilocode balance:", error)
return false
}
}

// For all other providers, assume they are usable
return true
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ vi.mock("vscode", () => ({
uriScheme: "vscode",
openExternal: vi.fn(),
},
commands: {
executeCommand: vi.fn(),
},
}))

vi.mock("axios")
Expand Down
1 change: 1 addition & 0 deletions src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2032,6 +2032,7 @@ export const webviewMessageHandler = async (
await provider.providerSettingsManager.saveConfig(message.text, message.apiConfiguration)
const listApiConfig = await provider.providerSettingsManager.listConfig()
await updateGlobalState("listApiConfigMeta", listApiConfig)
vscode.commands.executeCommand("kilo-code.ghost.reload") // kilocode_change: Reload ghost model when API provider settings change
} catch (error) {
provider.log(
`Error save api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/ar/kilocode.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/i18n/locales/ca/kilocode.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/i18n/locales/cs/kilocode.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/i18n/locales/de/kilocode.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/i18n/locales/en/kilocode.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
"tokenError": "A valid token must be set to use Autocomplete",
"lastCompletion": "Last suggestion:",
"sessionTotal": "Session total cost:",
"provider": "Provider:",
"model": "Model:"
},
"cost": {
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/es/kilocode.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/i18n/locales/fr/kilocode.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/i18n/locales/hi/kilocode.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/i18n/locales/id/kilocode.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/i18n/locales/it/kilocode.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/i18n/locales/ja/kilocode.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/i18n/locales/ko/kilocode.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/i18n/locales/nl/kilocode.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/i18n/locales/pl/kilocode.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/i18n/locales/pt-BR/kilocode.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/i18n/locales/ru/kilocode.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/i18n/locales/th/kilocode.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/i18n/locales/tr/kilocode.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/i18n/locales/uk/kilocode.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading