diff --git a/packages/types/src/__tests__/kilocode.test.ts b/packages/types/src/__tests__/kilocode.test.ts index d69d1aeedb9..63983d1300a 100644 --- a/packages/types/src/__tests__/kilocode.test.ts +++ b/packages/types/src/__tests__/kilocode.test.ts @@ -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", () => { @@ -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 + + 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) + }) +}) diff --git a/packages/types/src/kilocode/kilocode.ts b/packages/types/src/kilocode/kilocode.ts index 7da68614c77..11506d2fb15 100644 --- a/packages/types/src/kilocode/kilocode.ts +++ b/packages/types/src/kilocode/kilocode.ts @@ -1,4 +1,5 @@ import { z } from "zod" +import { ProviderSettings, ProviderSettingsEntry } from "../provider-settings.js" export const ghostServiceSettingsSchema = z .object({ @@ -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() @@ -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 - True if balance > 0, false otherwise + */ +export async function checkKilocodeBalance(kilocodeToken: string, kilocodeOrganizationId?: string): Promise { + try { + const baseUrl = getKiloBaseUriFromToken(kilocodeToken) + + const headers: Record = { + 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 + 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 + getProfile(params: { id: string }): Promise +} + +export type ProviderUsabilityChecker = ( + provider: AutocompleteProviderKey, + providerSettingsManager: ProviderSettingsManager, +) => Promise + +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 +} diff --git a/src/core/webview/__tests__/webviewMessageHandler.autoSwitch.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.autoSwitch.spec.ts index 9db7fdd2a1c..ef3b997ea39 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.autoSwitch.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.autoSwitch.spec.ts @@ -27,6 +27,9 @@ vi.mock("vscode", () => ({ uriScheme: "vscode", openExternal: vi.fn(), }, + commands: { + executeCommand: vi.fn(), + }, })) vi.mock("axios") diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 3845816e748..c98064e7617 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -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)}`, diff --git a/src/i18n/locales/ar/kilocode.json b/src/i18n/locales/ar/kilocode.json index fbd5a357a21..0e06037a1ba 100644 --- a/src/i18n/locales/ar/kilocode.json +++ b/src/i18n/locales/ar/kilocode.json @@ -77,6 +77,7 @@ "tokenError": "يجب تعيين رمز صالح لاستخدام الإكمال التلقائي", "lastCompletion": "آخر اقتراح:", "sessionTotal": "إجمالي تكلفة الجلسة:", + "provider": "المزود:", "model": "النموذج:" }, "cost": { diff --git a/src/i18n/locales/ca/kilocode.json b/src/i18n/locales/ca/kilocode.json index 3ac7c47c957..77893fa76de 100644 --- a/src/i18n/locales/ca/kilocode.json +++ b/src/i18n/locales/ca/kilocode.json @@ -73,6 +73,7 @@ "tokenError": "S'ha d'establir un token vàlid per utilitzar Autocomplete", "lastCompletion": "Últim suggeriment:", "sessionTotal": "Cost total de la sessió:", + "provider": "Proveïdor:", "model": "Model:" }, "cost": { diff --git a/src/i18n/locales/cs/kilocode.json b/src/i18n/locales/cs/kilocode.json index 210aec58f1f..d9bb3fc866b 100644 --- a/src/i18n/locales/cs/kilocode.json +++ b/src/i18n/locales/cs/kilocode.json @@ -79,6 +79,7 @@ "tokenError": "Pro použití Autocomplete musí být nastaven platný token", "lastCompletion": "Poslední návrh:", "sessionTotal": "Celkové náklady relace:", + "provider": "Poskytovatel:", "model": "Model:" }, "cost": { diff --git a/src/i18n/locales/de/kilocode.json b/src/i18n/locales/de/kilocode.json index f29c21504e2..2d923c98831 100644 --- a/src/i18n/locales/de/kilocode.json +++ b/src/i18n/locales/de/kilocode.json @@ -73,6 +73,7 @@ "tokenError": "Ein gültiger Token muss gesetzt werden, um Autocomplete zu verwenden", "lastCompletion": "Letzter Vorschlag:", "sessionTotal": "Sitzungsgesamtkosten:", + "provider": "Anbieter:", "model": "Modell:" }, "cost": { diff --git a/src/i18n/locales/en/kilocode.json b/src/i18n/locales/en/kilocode.json index a9c72d0c2a4..8ddcd0656e8 100644 --- a/src/i18n/locales/en/kilocode.json +++ b/src/i18n/locales/en/kilocode.json @@ -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": { diff --git a/src/i18n/locales/es/kilocode.json b/src/i18n/locales/es/kilocode.json index 511e67de6a8..5471dc8d0ea 100644 --- a/src/i18n/locales/es/kilocode.json +++ b/src/i18n/locales/es/kilocode.json @@ -73,6 +73,7 @@ "tokenError": "Se debe establecer un token válido para usar Autocomplete", "lastCompletion": "Última sugerencia:", "sessionTotal": "Costo total de la sesión:", + "provider": "Proveedor:", "model": "Modelo:" }, "cost": { diff --git a/src/i18n/locales/fr/kilocode.json b/src/i18n/locales/fr/kilocode.json index 3559aa06431..0218c6af9e5 100644 --- a/src/i18n/locales/fr/kilocode.json +++ b/src/i18n/locales/fr/kilocode.json @@ -73,6 +73,7 @@ "tokenError": "Un token valide doit être défini pour utiliser Autocomplete", "lastCompletion": "Dernière suggestion :", "sessionTotal": "Coût total de la session :", + "provider": "Fournisseur:", "model": "Modèle :" }, "cost": { diff --git a/src/i18n/locales/hi/kilocode.json b/src/i18n/locales/hi/kilocode.json index 1e53c8a82ad..8c8d802b212 100644 --- a/src/i18n/locales/hi/kilocode.json +++ b/src/i18n/locales/hi/kilocode.json @@ -79,6 +79,7 @@ "tokenError": "Autocomplete का उपयोग करने के लिए एक वैध टोकन सेट करना होगा", "lastCompletion": "अंतिम सुझाव:", "sessionTotal": "सत्र की कुल लागत:", + "provider": "प्रदाता:", "model": "मॉडल:" }, "cost": { diff --git a/src/i18n/locales/id/kilocode.json b/src/i18n/locales/id/kilocode.json index d43fe5f8651..f08ba948a43 100644 --- a/src/i18n/locales/id/kilocode.json +++ b/src/i18n/locales/id/kilocode.json @@ -73,6 +73,7 @@ "tokenError": "Token yang valid harus diatur untuk menggunakan Autocomplete", "lastCompletion": "Saran terakhir:", "sessionTotal": "Total biaya sesi:", + "provider": "Penyedia:", "model": "Model:" }, "cost": { diff --git a/src/i18n/locales/it/kilocode.json b/src/i18n/locales/it/kilocode.json index 105fe5fdb20..0191498a2ec 100644 --- a/src/i18n/locales/it/kilocode.json +++ b/src/i18n/locales/it/kilocode.json @@ -73,6 +73,7 @@ "tokenError": "Deve essere impostato un token valido per usare Autocomplete", "lastCompletion": "Ultimo suggerimento:", "sessionTotal": "Costo totale della sessione:", + "provider": "Provider:", "model": "Modello:" }, "cost": { diff --git a/src/i18n/locales/ja/kilocode.json b/src/i18n/locales/ja/kilocode.json index 6b1797a7610..baad36f6838 100644 --- a/src/i18n/locales/ja/kilocode.json +++ b/src/i18n/locales/ja/kilocode.json @@ -73,6 +73,7 @@ "tokenError": "Autocompleteを使用するには有効なトークンを設定する必要があります", "lastCompletion": "最後の提案:", "sessionTotal": "セッション合計コスト:", + "provider": "プロバイダー:", "model": "モデル:" }, "cost": { diff --git a/src/i18n/locales/ko/kilocode.json b/src/i18n/locales/ko/kilocode.json index e68aba9bd8a..f069c82a6f5 100644 --- a/src/i18n/locales/ko/kilocode.json +++ b/src/i18n/locales/ko/kilocode.json @@ -73,6 +73,7 @@ "tokenError": "Autocomplete를 사용하려면 유효한 토큰을 설정해야 합니다", "lastCompletion": "마지막 제안:", "sessionTotal": "세션 총 비용:", + "provider": "제공업체:", "model": "모델:" }, "cost": { diff --git a/src/i18n/locales/nl/kilocode.json b/src/i18n/locales/nl/kilocode.json index 106360dc285..8296608c60a 100644 --- a/src/i18n/locales/nl/kilocode.json +++ b/src/i18n/locales/nl/kilocode.json @@ -73,6 +73,7 @@ "tokenError": "Een geldig token moet worden ingesteld om Autocomplete te gebruiken", "lastCompletion": "Laatste suggestie:", "sessionTotal": "Totale sessiekosten:", + "provider": "Provider:", "model": "Model:" }, "cost": { diff --git a/src/i18n/locales/pl/kilocode.json b/src/i18n/locales/pl/kilocode.json index 22bc4aa445d..e76f9e1e354 100644 --- a/src/i18n/locales/pl/kilocode.json +++ b/src/i18n/locales/pl/kilocode.json @@ -79,6 +79,7 @@ "tokenError": "Aby używać Autocomplete, musi być ustawiony ważny token", "lastCompletion": "Ostatnia sugestia:", "sessionTotal": "Całkowity koszt sesji:", + "provider": "Dostawca:", "model": "Model:" }, "cost": { diff --git a/src/i18n/locales/pt-BR/kilocode.json b/src/i18n/locales/pt-BR/kilocode.json index 821627c6e8c..a3494e0bbcc 100644 --- a/src/i18n/locales/pt-BR/kilocode.json +++ b/src/i18n/locales/pt-BR/kilocode.json @@ -73,6 +73,7 @@ "tokenError": "Um token válido deve ser definido para usar o Autocomplete", "lastCompletion": "Última sugestão:", "sessionTotal": "Custo total da sessão:", + "provider": "Provedor:", "model": "Modelo:" }, "cost": { diff --git a/src/i18n/locales/ru/kilocode.json b/src/i18n/locales/ru/kilocode.json index d538f6f9947..3fbc0bc4149 100644 --- a/src/i18n/locales/ru/kilocode.json +++ b/src/i18n/locales/ru/kilocode.json @@ -68,6 +68,7 @@ "tokenError": "Для использования Autocomplete необходимо установить действительный токен", "lastCompletion": "Последнее предложение:", "sessionTotal": "Общая стоимость сессии:", + "provider": "Провайдер:", "model": "Модель:" }, "cost": { diff --git a/src/i18n/locales/th/kilocode.json b/src/i18n/locales/th/kilocode.json index d9fb77c77a7..680dd38f0de 100644 --- a/src/i18n/locales/th/kilocode.json +++ b/src/i18n/locales/th/kilocode.json @@ -79,6 +79,7 @@ "tokenError": "ต้องตั้งค่าโทเค็นที่ถูกต้องเพื่อใช้ Autocomplete", "lastCompletion": "คำแนะนำล่าสุด:", "sessionTotal": "ค่าใช้จ่ายรวมของเซสชัน:", + "provider": "ผู้ให้บริการ:", "model": "โมเดล:" }, "cost": { diff --git a/src/i18n/locales/tr/kilocode.json b/src/i18n/locales/tr/kilocode.json index d5bc68bdf48..4d52a9b5e00 100644 --- a/src/i18n/locales/tr/kilocode.json +++ b/src/i18n/locales/tr/kilocode.json @@ -73,6 +73,7 @@ "tokenError": "Autocomplete kullanmak için geçerli bir token ayarlanmalı", "lastCompletion": "Son öneri:", "sessionTotal": "Oturum toplam maliyeti:", + "provider": "Sağlayıcı:", "model": "Model:" }, "cost": { diff --git a/src/i18n/locales/uk/kilocode.json b/src/i18n/locales/uk/kilocode.json index 7a74da1b72b..0eedcea7650 100644 --- a/src/i18n/locales/uk/kilocode.json +++ b/src/i18n/locales/uk/kilocode.json @@ -73,6 +73,7 @@ "tokenError": "Для використання Autocomplete потрібно встановити дійсний токен", "lastCompletion": "Остання пропозиція:", "sessionTotal": "Загальна вартість сесії:", + "provider": "Провайдер:", "model": "Модель:" }, "cost": { diff --git a/src/i18n/locales/vi/kilocode.json b/src/i18n/locales/vi/kilocode.json index 44e843e77c3..8397725927e 100644 --- a/src/i18n/locales/vi/kilocode.json +++ b/src/i18n/locales/vi/kilocode.json @@ -73,6 +73,7 @@ "tokenError": "Phải đặt token hợp lệ để sử dụng Autocomplete", "lastCompletion": "Gợi ý cuối cùng:", "sessionTotal": "Tổng chi phí phiên:", + "provider": "Nhà cung cấp:", "model": "Mô hình:" }, "cost": { diff --git a/src/i18n/locales/zh-CN/kilocode.json b/src/i18n/locales/zh-CN/kilocode.json index afe9d323fe5..ee461b9de38 100644 --- a/src/i18n/locales/zh-CN/kilocode.json +++ b/src/i18n/locales/zh-CN/kilocode.json @@ -79,6 +79,7 @@ "tokenError": "必须设置有效 Token 才能使用 Autocomplete", "lastCompletion": "最后建议:", "sessionTotal": "会话总费用:", + "provider": "提供商:", "model": "模型:" }, "cost": { diff --git a/src/i18n/locales/zh-TW/kilocode.json b/src/i18n/locales/zh-TW/kilocode.json index 78b516060f3..798553dfddd 100644 --- a/src/i18n/locales/zh-TW/kilocode.json +++ b/src/i18n/locales/zh-TW/kilocode.json @@ -73,6 +73,7 @@ "tokenError": "必須設定有效 Token 才能使用 Autocomplete", "lastCompletion": "最後建議:", "sessionTotal": "工作階段總費用:", + "provider": "提供者:", "model": "模型:" }, "cost": { diff --git a/src/services/ghost/GhostModel.ts b/src/services/ghost/GhostModel.ts index fae2d99e01f..a69f82e668d 100644 --- a/src/services/ghost/GhostModel.ts +++ b/src/services/ghost/GhostModel.ts @@ -1,15 +1,14 @@ -import { GhostServiceSettings } from "@roo-code/types" +import { + AUTOCOMPLETE_PROVIDER_MODELS, + defaultProviderUsabilityChecker, + modelIdKeysByProvider, + ProviderSettingsEntry, +} from "@roo-code/types" import { ApiHandler, buildApiHandler } from "../../api" -import { ContextProxy } from "../../core/config/ContextProxy" import { ProviderSettingsManager } from "../../core/config/ProviderSettingsManager" import { OpenRouterHandler } from "../../api/providers" import { ApiStreamChunk } from "../../api/transform/stream" -const KILOCODE_DEFAULT_MODEL = "mistralai/codestral-2508" -const MISTRAL_DEFAULT_MODEL = "codestral-latest" - -const SUPPORTED_DEFAULT_PROVIDERS = ["mistral", "kilocode", "openrouter"] - export class GhostModel { private apiHandler: ApiHandler | null = null public loaded = false @@ -20,55 +19,55 @@ export class GhostModel { this.loaded = true } } + private cleanup(): void { + this.apiHandler = null + this.loaded = false + } - public async reload(settings: GhostServiceSettings, providerSettingsManager: ProviderSettingsManager) { + public async reload(providerSettingsManager: ProviderSettingsManager): Promise { const profiles = await providerSettingsManager.listConfig() - const validProfiles = profiles - .filter((x) => x.apiProvider && SUPPORTED_DEFAULT_PROVIDERS.includes(x.apiProvider)) - .sort((a, b) => { - if (!a.apiProvider) { - return 1 // Place undefined providers at the end - } - if (!b.apiProvider) { - return -1 // Place undefined providers at the beginning - } - return ( - SUPPORTED_DEFAULT_PROVIDERS.indexOf(a.apiProvider) - - SUPPORTED_DEFAULT_PROVIDERS.indexOf(b.apiProvider) - ) - }) - - const selectedProfile = validProfiles[0] || null - if (selectedProfile) { - const profile = await providerSettingsManager.getProfile({ - id: selectedProfile.id, - }) - const profileProvider = profile.apiProvider - let modelDefinition = {} - if (profileProvider === "kilocode") { - modelDefinition = { - kilocodeModel: KILOCODE_DEFAULT_MODEL, - } - } else if (profileProvider === "openrouter") { - modelDefinition = { - openRouterModelId: KILOCODE_DEFAULT_MODEL, - } - } else if (profileProvider === "mistral") { - modelDefinition = { - apiModelId: MISTRAL_DEFAULT_MODEL, - } + const supportedProviders = Object.keys(AUTOCOMPLETE_PROVIDER_MODELS) as Array< + keyof typeof AUTOCOMPLETE_PROVIDER_MODELS + > + + this.cleanup() + + // Check providers in order, but skip unusable ones (e.g., kilocode with zero balance) + for (const provider of supportedProviders) { + const selectedProfile = profiles.find( + (x): x is typeof x & { apiProvider: string } => x?.apiProvider === provider, + ) + if (selectedProfile) { + const isUsable = await defaultProviderUsabilityChecker(provider, providerSettingsManager) + if (!isUsable) continue + + this.loadProfile(providerSettingsManager, selectedProfile, provider) + this.loaded = true + return true } - this.apiHandler = buildApiHandler({ - ...profile, - ...modelDefinition, - }) } + this.loaded = true // we loaded, and found nothing, but we do not wish to reload + return false + } + + public async loadProfile( + providerSettingsManager: ProviderSettingsManager, + selectedProfile: ProviderSettingsEntry, + provider: keyof typeof AUTOCOMPLETE_PROVIDER_MODELS, + ): Promise { + const profile = await providerSettingsManager.getProfile({ + id: selectedProfile.id, + }) + + this.apiHandler = buildApiHandler({ + ...profile, + [modelIdKeysByProvider[provider]]: AUTOCOMPLETE_PROVIDER_MODELS[provider], + }) + if (this.apiHandler instanceof OpenRouterHandler) { await this.apiHandler.fetchModel() } - - this.loaded = true } /** @@ -131,13 +130,22 @@ export class GhostModel { } public getModelName(): string | null { - if (!this.apiHandler) { - return null - } - // Extract model name from API handler + if (!this.apiHandler) return null + return this.apiHandler.getModel().id ?? "unknown" } + public getProviderDisplayName(): string | null { + if (!this.apiHandler) return null + + const handler = this.apiHandler as any + if (handler.providerName && typeof handler.providerName === "string") { + return handler.providerName + } else { + return "unknown" + } + } + public hasValidCredentials(): boolean { return this.apiHandler !== null && this.loaded } diff --git a/src/services/ghost/GhostProvider.ts b/src/services/ghost/GhostProvider.ts index d4bea3cd9c9..ab320a62e96 100644 --- a/src/services/ghost/GhostProvider.ts +++ b/src/services/ghost/GhostProvider.ts @@ -118,16 +118,22 @@ export class GhostProvider { if (!this.settings) { return } - await ContextProxy.instance?.setValues?.({ ghostServiceSettings: this.settings }) + const settingsWithModelInfo = { + ...this.settings, + provider: this.getCurrentProviderName(), + model: this.getCurrentModelName(), + } + await ContextProxy.instance?.setValues?.({ ghostServiceSettings: settingsWithModelInfo }) await this.cline.postStateToWebview() } public async load() { this.settings = this.loadSettings() - await this.model.reload(this.settings, this.providerSettingsManager) + await this.model.reload(this.providerSettingsManager) this.cursorAnimation.updateSettings(this.settings || undefined) await this.updateGlobalContext() this.updateStatusBar() + await this.saveSettings() } public async disable() { @@ -586,6 +592,7 @@ export class GhostProvider { this.statusBar = new GhostStatusBar({ enabled: false, model: "loading...", + provider: "loading...", hasValidToken: false, totalSessionCost: 0, lastCompletionCost: 0, @@ -599,6 +606,13 @@ export class GhostProvider { return this.model.getModelName() ?? "unknown" } + private getCurrentProviderName(): string { + if (!this.model.loaded) { + return "loading..." + } + return this.model.getProviderDisplayName() ?? "unknown" + } + private hasValidApiToken(): boolean { return this.model.loaded && this.model.hasValidCredentials() } @@ -617,6 +631,7 @@ export class GhostProvider { this.statusBar?.update({ enabled: this.settings?.enableAutoTrigger, model: this.getCurrentModelName(), + provider: this.getCurrentProviderName(), hasValidToken: this.hasValidApiToken(), totalSessionCost: this.sessionCost, lastCompletionCost: this.lastCompletionCost, diff --git a/src/services/ghost/GhostStatusBar.ts b/src/services/ghost/GhostStatusBar.ts index 8c3e50fecf7..6b536749667 100644 --- a/src/services/ghost/GhostStatusBar.ts +++ b/src/services/ghost/GhostStatusBar.ts @@ -4,6 +4,7 @@ import { t } from "../../i18n" interface GhostStatusBarStateProps { enabled?: boolean model?: string + provider?: string hasValidToken?: boolean totalSessionCost?: number lastCompletionCost?: number @@ -13,6 +14,7 @@ export class GhostStatusBar { statusBar: vscode.StatusBarItem enabled: boolean model: string + provider: string hasValidToken: boolean totalSessionCost?: number lastCompletionCost?: number @@ -21,6 +23,7 @@ export class GhostStatusBar { this.statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 100) this.enabled = params.enabled || false this.model = params.model || "default" + this.provider = params.provider || "default" this.hasValidToken = params.hasValidToken || false this.totalSessionCost = params.totalSessionCost this.lastCompletionCost = params.lastCompletionCost @@ -55,6 +58,7 @@ export class GhostStatusBar { public update(params: GhostStatusBarStateProps) { this.enabled = params.enabled !== undefined ? params.enabled : this.enabled this.model = params.model !== undefined ? params.model : this.model + this.provider = params.provider !== undefined ? params.provider : this.provider this.hasValidToken = params.hasValidToken !== undefined ? params.hasValidToken : this.hasValidToken this.totalSessionCost = params.totalSessionCost !== undefined ? params.totalSessionCost : this.totalSessionCost this.lastCompletionCost = @@ -64,12 +68,6 @@ export class GhostStatusBar { if (this.enabled) this.render() } - // TODO: Bring back paused state in the future - // private renderPaused() { - // this.statusBar.text = t("kilocode:ghost.statusBar.disabled") - // this.statusBar.tooltip = t("kilocode:ghost.statusBar.tooltip.disabled") - // } - private renderTokenError() { this.statusBar.text = t("kilocode:ghost.statusBar.warning") this.statusBar.tooltip = t("kilocode:ghost.statusBar.tooltip.tokenError") @@ -83,6 +81,7 @@ export class GhostStatusBar { ${t("kilocode:ghost.statusBar.tooltip.basic")} • ${t("kilocode:ghost.statusBar.tooltip.lastCompletion")} $${lastCompletionCostFormatted} • ${t("kilocode:ghost.statusBar.tooltip.sessionTotal")} ${totalCostFormatted} +• ${t("kilocode:ghost.statusBar.tooltip.provider")} ${this.provider} • ${t("kilocode:ghost.statusBar.tooltip.model")} ${this.model}\ ` } diff --git a/src/services/ghost/__tests__/GhostModel.spec.ts b/src/services/ghost/__tests__/GhostModel.spec.ts new file mode 100644 index 00000000000..8e8dfee7bf1 --- /dev/null +++ b/src/services/ghost/__tests__/GhostModel.spec.ts @@ -0,0 +1,325 @@ +import { describe, it, expect, vi, beforeEach } from "vitest" +import { GhostModel } from "../GhostModel" +import { ProviderSettingsManager } from "../../../core/config/ProviderSettingsManager" +import { AUTOCOMPLETE_PROVIDER_MODELS } from "@roo-code/types" + +describe("GhostModel", () => { + let mockProviderSettingsManager: ProviderSettingsManager + + beforeEach(() => { + mockProviderSettingsManager = { + listConfig: vi.fn(), + getProfile: vi.fn(), + } as any + }) + + describe("reload", () => { + it("sorts profiles by supportedProviders index order", async () => { + const supportedProviders = Object.keys(AUTOCOMPLETE_PROVIDER_MODELS) + const profiles = [ + { id: "3", name: "profile3", apiProvider: supportedProviders[2] }, + { id: "1", name: "profile1", apiProvider: supportedProviders[0] }, + { id: "2", name: "profile2", apiProvider: supportedProviders[1] }, + ] as any + + vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue(profiles) + vi.mocked(mockProviderSettingsManager.getProfile).mockResolvedValue({ + id: "1", + name: "profile1", + apiProvider: supportedProviders[0], + mistralApiKey: "test-key", + } as any) + + const model = new GhostModel() + await model.reload(mockProviderSettingsManager) + + expect(mockProviderSettingsManager.getProfile).toHaveBeenCalledWith({ id: "1" }) + }) + + it("filters out profiles without apiProvider", async () => { + const supportedProviders = Object.keys(AUTOCOMPLETE_PROVIDER_MODELS) + const profiles = [ + { id: "1", name: "profile1", apiProvider: undefined }, + { id: "2", name: "profile2", apiProvider: supportedProviders[0] }, + ] as any + + vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue(profiles) + vi.mocked(mockProviderSettingsManager.getProfile).mockResolvedValue({ + id: "2", + name: "profile2", + apiProvider: supportedProviders[0], + mistralApiKey: "test-key", + } as any) + + const model = new GhostModel() + await model.reload(mockProviderSettingsManager) + + expect(mockProviderSettingsManager.getProfile).toHaveBeenCalledWith({ id: "2" }) + }) + + it("filters out profiles with unsupported apiProvider", async () => { + const supportedProviders = Object.keys(AUTOCOMPLETE_PROVIDER_MODELS) + const profiles = [ + { id: "1", name: "profile1", apiProvider: "unsupported" }, + { id: "2", name: "profile2", apiProvider: supportedProviders[0] }, + ] as any + + vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue(profiles) + vi.mocked(mockProviderSettingsManager.getProfile).mockResolvedValue({ + id: "2", + name: "profile2", + apiProvider: supportedProviders[0], + mistralApiKey: "test-key", + } as any) + + const model = new GhostModel() + await model.reload(mockProviderSettingsManager) + + expect(mockProviderSettingsManager.getProfile).toHaveBeenCalledWith({ id: "2" }) + }) + + it("handles empty profile list", async () => { + vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue([]) + + const model = new GhostModel() + const result = await model.reload(mockProviderSettingsManager) + + expect(mockProviderSettingsManager.getProfile).not.toHaveBeenCalled() + expect(model.hasValidCredentials()).toBe(false) + expect(result).toBe(false) + }) + + it("returns true when profile found", async () => { + const supportedProviders = Object.keys(AUTOCOMPLETE_PROVIDER_MODELS) + const profiles = [{ id: "1", name: "profile1", apiProvider: supportedProviders[0] }] as any + + vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue(profiles) + vi.mocked(mockProviderSettingsManager.getProfile).mockResolvedValue({ + id: "1", + name: "profile1", + apiProvider: supportedProviders[0], + mistralApiKey: "test-key", + } as any) + + const model = new GhostModel() + const result = await model.reload(mockProviderSettingsManager) + + expect(result).toBe(true) + expect(model.loaded).toBe(true) + }) + }) + + describe("provider usability", () => { + beforeEach(() => { + // Mock fetch globally for these tests + vi.stubGlobal("fetch", vi.fn()) + }) + + afterEach(() => { + // Restore fetch + vi.unstubAllGlobals() + }) + + it("should skip kilocode provider when balance is zero and use openrouter instead", async () => { + const profiles = [ + { id: "1", name: "kilocode-profile", apiProvider: "kilocode" }, + { id: "2", name: "openrouter-profile", apiProvider: "openrouter" }, + ] as any + + vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue(profiles) + + // Mock profiles with tokens + vi.mocked(mockProviderSettingsManager.getProfile).mockImplementation(async (args: any) => { + if (args.id === "1") { + return { + id: "1", + name: "kilocode-profile", + apiProvider: "kilocode", + kilocodeToken: "test-token", + } as any + } else if (args.id === "2") { + return { + id: "2", + name: "openrouter-profile", + apiProvider: "openrouter", + openRouterApiKey: "test-key", + } as any + } + return null as any + }) + + // Mock fetch to return zero balance for kilocode + ;(global.fetch as any).mockImplementation(async (url: string) => { + if (url.includes("/api/profile/balance")) { + return { + ok: true, + json: async () => ({ data: { balance: 0 } }), + } as any + } + // For OpenRouter models endpoint + if (url.includes("/models")) { + return { + ok: true, + json: async () => ({ data: [] }), + } as any + } + // For other URLs, return a basic response + return { + ok: true, + json: async () => ({}), + } as any + }) + + const model = new GhostModel() + const result = await model.reload(mockProviderSettingsManager) + + // Should have tried both providers but used openrouter (since kilocode balance is 0) + expect(result).toBe(true) + expect(model.loaded).toBe(true) + }) + + it("should use kilocode provider when balance is greater than zero", async () => { + const profiles = [ + { id: "1", name: "kilocode-profile", apiProvider: "kilocode" }, + { id: "2", name: "openrouter-profile", apiProvider: "openrouter" }, + ] as any + + vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue(profiles) + + // Mock profiles with tokens + vi.mocked(mockProviderSettingsManager.getProfile).mockImplementation(async (args: any) => { + if (args.id === "1") { + return { + id: "1", + name: "kilocode-profile", + apiProvider: "kilocode", + kilocodeToken: "test-token", + } as any + } else if (args.id === "2") { + return { + id: "2", + name: "openrouter-profile", + apiProvider: "openrouter", + openRouterApiKey: "test-key", + } as any + } + return null as any + }) + + // Mock fetch to return positive balance for kilocode + ;(global.fetch as any).mockImplementation(async (url: string) => { + if (url.includes("/api/profile/balance")) { + return { + ok: true, + json: async () => ({ data: { balance: 10.5 } }), + } as any + } + // For OpenRouter models endpoint + if (url.includes("/models")) { + return { + ok: true, + json: async () => ({ data: [] }), + } as any + } + // For other URLs, return a basic response + return { + ok: true, + json: async () => ({}), + } as any + }) + + const model = new GhostModel() + const result = await model.reload(mockProviderSettingsManager) + + // Should have used kilocode provider (first one with positive balance) + expect(result).toBe(true) + expect(model.loaded).toBe(true) + }) + + it("should handle kilocode provider with no token", async () => { + const profiles = [ + { id: "1", name: "kilocode-profile", apiProvider: "kilocode" }, + { id: "2", name: "openrouter-profile", apiProvider: "openrouter" }, + ] as any + + vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue(profiles) + + // Mock profiles - kilocode without token + vi.mocked(mockProviderSettingsManager.getProfile).mockImplementation(async (args: any) => { + if (args.id === "1") { + return { + id: "1", + name: "kilocode-profile", + apiProvider: "kilocode", + kilocodeToken: "", // No token + } as any + } else if (args.id === "2") { + return { + id: "2", + name: "openrouter-profile", + apiProvider: "openrouter", + openRouterApiKey: "test-key", + } as any + } + return null as any + }) + + // Mock fetch to handle the no-token case + ;(global.fetch as any).mockImplementation(async (url: string) => { + if (url.includes("/api/profile/balance")) { + // This should not be called since there's no token + return { + ok: false, + status: 401, + } as any + } + // For OpenRouter models endpoint + if (url.includes("/models")) { + return { + ok: true, + json: async () => ({ data: [] }), + } as any + } + // For other URLs, return a basic response + return { + ok: true, + json: async () => ({}), + } as any + }) + + const model = new GhostModel() + const result = await model.reload(mockProviderSettingsManager) + + // Should skip kilocode (no token) and use openrouter + expect(result).toBe(true) + expect(model.loaded).toBe(true) + }) + }) + + describe("getProviderDisplayName", () => { + it("returns null when no provider is loaded", () => { + const model = new GhostModel() + expect(model.getProviderDisplayName()).toBeNull() + }) + + it("returns provider name from API handler when provider is loaded", async () => { + const supportedProviders = Object.keys(AUTOCOMPLETE_PROVIDER_MODELS) + const profiles = [{ id: "1", name: "profile1", apiProvider: supportedProviders[0] }] as any + + vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue(profiles) + vi.mocked(mockProviderSettingsManager.getProfile).mockResolvedValue({ + id: "1", + name: "profile1", + apiProvider: supportedProviders[0], + mistralApiKey: "test-key", + } as any) + + const model = new GhostModel() + await model.reload(mockProviderSettingsManager) + + const providerName = model.getProviderDisplayName() + expect(providerName).toBeTruthy() + expect(typeof providerName).toBe("string") + }) + }) +}) diff --git a/webview-ui/src/components/kilocode/settings/GhostServiceSettings.tsx b/webview-ui/src/components/kilocode/settings/GhostServiceSettings.tsx index 49cee317d84..1a03b6e1cbb 100644 --- a/webview-ui/src/components/kilocode/settings/GhostServiceSettings.tsx +++ b/webview-ui/src/components/kilocode/settings/GhostServiceSettings.tsx @@ -26,8 +26,14 @@ export const GhostServiceSettingsView = ({ ...props }: GhostServiceSettingsViewProps) => { const { t } = useAppTranslation() - const { enableAutoTrigger, autoTriggerDelay, enableQuickInlineTaskKeybinding, enableSmartInlineTaskKeybinding } = - ghostServiceSettings || {} + const { + enableAutoTrigger, + autoTriggerDelay, + enableQuickInlineTaskKeybinding, + enableSmartInlineTaskKeybinding, + provider, + model, + } = ghostServiceSettings || {} const keybindings = useKeybindings(["kilo-code.addToContextAndFocus", "kilo-code.ghost.generateSuggestions"]) const normalizedDelay = normalizeAutoTriggerDelay(autoTriggerDelay) @@ -168,6 +174,34 @@ export const GhostServiceSettingsView = ({ /> + +
+
+ +
{t("kilocode:ghost.settings.model")}
+
+
+ +
+
+ {provider && model ? ( + <> +
+ {t("kilocode:ghost.settings.provider")}:{" "} + {provider} +
+
+ {t("kilocode:ghost.settings.model")}:{" "} + {model} +
+ + ) : ( +
+ {t("kilocode:ghost.settings.noModelConfigured")} +
+ )} +
+
diff --git a/webview-ui/src/components/kilocode/settings/__tests__/GhostServiceSettings.spec.tsx b/webview-ui/src/components/kilocode/settings/__tests__/GhostServiceSettings.spec.tsx index 4298bcd5ba1..bfdae3faef9 100644 --- a/webview-ui/src/components/kilocode/settings/__tests__/GhostServiceSettings.spec.tsx +++ b/webview-ui/src/components/kilocode/settings/__tests__/GhostServiceSettings.spec.tsx @@ -77,6 +77,8 @@ const defaultGhostServiceSettings: GhostServiceSettings = { autoTriggerDelay: 3, enableQuickInlineTaskKeybinding: false, enableSmartInlineTaskKeybinding: false, + provider: "openrouter", + model: "openai/gpt-4o-mini", } const renderComponent = (props = {}) => { @@ -240,4 +242,55 @@ describe("GhostServiceSettingsView", () => { // We should have multiple description divs for the different settings expect(descriptionDivs.length).toBeGreaterThan(2) }) + + it("displays provider and model information when available", () => { + renderComponent({ + ghostServiceSettings: { + ...defaultGhostServiceSettings, + provider: "openrouter", + model: "openai/gpt-4o-mini", + }, + }) + + expect(screen.getByText(/kilocode:ghost.settings.provider/)).toBeInTheDocument() + expect(screen.getByText(/openrouter/)).toBeInTheDocument() + expect(screen.getAllByText(/kilocode:ghost.settings.model/).length).toBeGreaterThan(0) + expect(screen.getByText(/openai\/gpt-4o-mini/)).toBeInTheDocument() + }) + + it("displays error message when provider and model are not configured", () => { + renderComponent({ + ghostServiceSettings: { + ...defaultGhostServiceSettings, + provider: undefined, + model: undefined, + }, + }) + + expect(screen.getByText(/kilocode:ghost.settings.noModelConfigured/)).toBeInTheDocument() + }) + + it("displays error message when only provider is missing", () => { + renderComponent({ + ghostServiceSettings: { + ...defaultGhostServiceSettings, + provider: undefined, + model: "openai/gpt-4o-mini", + }, + }) + + expect(screen.getByText(/kilocode:ghost.settings.noModelConfigured/)).toBeInTheDocument() + }) + + it("displays error message when only model is missing", () => { + renderComponent({ + ghostServiceSettings: { + ...defaultGhostServiceSettings, + provider: "openrouter", + model: undefined, + }, + }) + + expect(screen.getByText(/kilocode:ghost.settings.noModelConfigured/)).toBeInTheDocument() + }) }) diff --git a/webview-ui/src/i18n/locales/ar/kilocode.json b/webview-ui/src/i18n/locales/ar/kilocode.json index 7c5f6324a21..477eb2531a5 100644 --- a/webview-ui/src/i18n/locales/ar/kilocode.json +++ b/webview-ui/src/i18n/locales/ar/kilocode.json @@ -224,7 +224,10 @@ "label": "الإكمال التلقائي اليدوي ({{keybinding}})", "description": "تحتاج إصلاحاً سريعاً أو إكمالاً أو إعادة هيكلة؟ سيستخدم Kilo السياق المحيط لتقديم تحسينات فورية، مما يبقيك في التدفق. عرض الاختصار" }, - "keybindingNotFound": "غير موجود" + "keybindingNotFound": "غير موجود", + "noModelConfigured": "لم يتم العثور على نموذج إكمال تلقائي مناسب. يرجى تكوين مزود في إعدادات API.", + "model": "النموذج", + "provider": "المزود" } }, "virtualProvider": { diff --git a/webview-ui/src/i18n/locales/ca/kilocode.json b/webview-ui/src/i18n/locales/ca/kilocode.json index 017e91a8a09..599c6801f31 100644 --- a/webview-ui/src/i18n/locales/ca/kilocode.json +++ b/webview-ui/src/i18n/locales/ca/kilocode.json @@ -218,7 +218,10 @@ "label": "Autocompleció Manual ({{keybinding}})", "description": "Necessites una correcció ràpida, completació o refactorització? Kilo utilitzarà el context circumdant per oferir millores immediates, mantenint-te en el flux. Veure drecera" }, - "keybindingNotFound": "no trobat" + "keybindingNotFound": "no trobat", + "noModelConfigured": "No s'ha trobat cap model d'autocompletat adequat. Configura un proveïdor a la configuració de l'API.", + "model": "Model", + "provider": "Proveïdor" } }, "virtualProvider": { diff --git a/webview-ui/src/i18n/locales/cs/kilocode.json b/webview-ui/src/i18n/locales/cs/kilocode.json index 8573aeb948c..49e31606e6e 100644 --- a/webview-ui/src/i18n/locales/cs/kilocode.json +++ b/webview-ui/src/i18n/locales/cs/kilocode.json @@ -229,7 +229,10 @@ "label": "Ruční dokončování ({{keybinding}})", "description": "Potřebuješ rychlou opravu, dokončení nebo refaktoring? Kilo použije okolní kontext k nabídnutí okamžitých vylepšení, udržujíc tě v toku. Zobrazit zkratku" }, - "keybindingNotFound": "nenalezeno" + "keybindingNotFound": "nenalezeno", + "noModelConfigured": "Nebyl nalezen žádný vhodný model pro automatické dokončování. Nakonfiguruj prosím poskytovatele v nastavení API.", + "model": "Model", + "provider": "Poskytovatel" } }, "virtualProvider": { diff --git a/webview-ui/src/i18n/locales/de/kilocode.json b/webview-ui/src/i18n/locales/de/kilocode.json index 9a059eef595..e612d3a3fc5 100644 --- a/webview-ui/src/i18n/locales/de/kilocode.json +++ b/webview-ui/src/i18n/locales/de/kilocode.json @@ -208,6 +208,8 @@ "title": "Autocomplete", "settings": { "triggers": "Auslöser", + "model": "Modell", + "provider": "Provider", "enableAutoTrigger": { "label": "Pausieren zum Vervollständigen", "description": "Wenn aktiviert, löst Kilo Code automatisch Autocomplete aus, wenn du aufhörst zu tippen. Dies kann für schnelle Korrekturen und Vorschläge nützlich sein." @@ -224,7 +226,8 @@ "label": "Manuelle Autovervollständigung ({{keybinding}})", "description": "Brauchst du eine schnelle Korrektur, Vervollständigung oder Refaktorierung? Kilo wird den umgebenden Kontext nutzen, um sofortige Verbesserungen anzubieten und dich im Flow zu halten. Tastenkombination bearbeiten" }, - "keybindingNotFound": "nicht gefunden" + "keybindingNotFound": "nicht gefunden", + "noModelConfigured": "Kein geeignetes Autocomplete-Modell gefunden. Bitte konfiguriere einen Provider in den API-Einstellungen." } }, "virtualProvider": { diff --git a/webview-ui/src/i18n/locales/en/kilocode.json b/webview-ui/src/i18n/locales/en/kilocode.json index 6ccc09a6c9b..3863677ac86 100644 --- a/webview-ui/src/i18n/locales/en/kilocode.json +++ b/webview-ui/src/i18n/locales/en/kilocode.json @@ -219,6 +219,8 @@ "title": "Autocomplete", "settings": { "triggers": "Triggers", + "model": "Model", + "provider": "Provider", "enableAutoTrigger": { "label": "Pause to Complete", "description": "When enabled, Kilo Code will automatically trigger Autocomplete when you pause typing. This can be useful for quick fixes and suggestions." @@ -235,7 +237,8 @@ "label": "Manual Autocomplete ({{keybinding}})", "description": "Need a quick fix, completion, or refactor? Kilo will use the surrounding context to offer immediate improvements, keeping you in the flow. Edit shortcut" }, - "keybindingNotFound": "not found" + "keybindingNotFound": "not found", + "noModelConfigured": "No suitable autocomplete model found. Please configure a provider in the API settings." } }, "virtualProvider": { diff --git a/webview-ui/src/i18n/locales/es/kilocode.json b/webview-ui/src/i18n/locales/es/kilocode.json index c053377c899..267c0ce2292 100644 --- a/webview-ui/src/i18n/locales/es/kilocode.json +++ b/webview-ui/src/i18n/locales/es/kilocode.json @@ -222,7 +222,10 @@ "label": "Autocompletado Manual ({{keybinding}})", "description": "¿Necesitas una corrección rápida, completado o refactorización? Kilo usará el contexto circundante para ofrecer mejoras inmediatas, manteniéndote en el flujo. Editar atajo" }, - "keybindingNotFound": "no encontrado" + "keybindingNotFound": "no encontrado", + "noModelConfigured": "No se encontró ningún modelo de autocompletado adecuado. Por favor, configura un proveedor en la configuración de API.", + "model": "Modelo", + "provider": "Proveedor" } }, "virtualProvider": { diff --git a/webview-ui/src/i18n/locales/fr/kilocode.json b/webview-ui/src/i18n/locales/fr/kilocode.json index 2e4a0ef5a50..e7eb8cfa327 100644 --- a/webview-ui/src/i18n/locales/fr/kilocode.json +++ b/webview-ui/src/i18n/locales/fr/kilocode.json @@ -229,7 +229,10 @@ "label": "Saisie automatique manuelle ({{keybinding}})", "description": "Besoin d'une correction rapide, d'un complément ou d'une refactorisation ? Kilo utilisera le contexte environnant pour offrir des améliorations immédiates, te gardant dans le flux. Modifier le raccourci" }, - "keybindingNotFound": "introuvable" + "keybindingNotFound": "introuvable", + "noModelConfigured": "Aucun modèle d'autocomplétion approprié trouvé. Configure un fournisseur dans les paramètres API.", + "model": "Modèle", + "provider": "Fournisseur" } }, "virtualProvider": { diff --git a/webview-ui/src/i18n/locales/hi/kilocode.json b/webview-ui/src/i18n/locales/hi/kilocode.json index 148a569fd44..2d693c562ee 100644 --- a/webview-ui/src/i18n/locales/hi/kilocode.json +++ b/webview-ui/src/i18n/locales/hi/kilocode.json @@ -218,7 +218,10 @@ "label": "मैनुअल ऑटोकंप्लीट ({{keybinding}})", "description": "त्वरित सुधार, पूर्णता, या रिफैक्टरिंग चाहिए? Kilo आसपास के संदर्भ का उपयोग करके तत्काल सुधार प्रदान करेगा, आपको प्रवाह में रखते हुए। शॉर्टकट देखें" }, - "keybindingNotFound": "नहीं मिला" + "keybindingNotFound": "नहीं मिला", + "noModelConfigured": "कोई उपयुक्त ऑटोकम्पलीट मॉडल नहीं मिला। कृपया API सेटिंग्स में एक प्रदाता कॉन्फ़िगर करें।", + "model": "मॉडल", + "provider": "प्रदाता" } }, "virtualProvider": { diff --git a/webview-ui/src/i18n/locales/id/kilocode.json b/webview-ui/src/i18n/locales/id/kilocode.json index 1d60b2670ee..559c219ccd8 100644 --- a/webview-ui/src/i18n/locales/id/kilocode.json +++ b/webview-ui/src/i18n/locales/id/kilocode.json @@ -218,7 +218,10 @@ "label": "Pelengkapan Otomatis Manual ({{keybinding}})", "description": "Perlu perbaikan cepat, penyelesaian, atau refaktor? Kilo akan menggunakan konteks sekitar untuk menawarkan perbaikan langsung, menjaga Anda tetap dalam alur. Lihat pintasan" }, - "keybindingNotFound": "tidak ditemukan" + "keybindingNotFound": "tidak ditemukan", + "noModelConfigured": "Tidak ditemukan model autocomplete yang sesuai. Silakan konfigurasi penyedia di pengaturan API.", + "model": "Model", + "provider": "Penyedia" } }, "virtualProvider": { diff --git a/webview-ui/src/i18n/locales/it/kilocode.json b/webview-ui/src/i18n/locales/it/kilocode.json index 34864575052..052e5588877 100644 --- a/webview-ui/src/i18n/locales/it/kilocode.json +++ b/webview-ui/src/i18n/locales/it/kilocode.json @@ -225,7 +225,10 @@ "label": "Completamento Automatico Manuale ({{keybinding}})", "description": "Hai bisogno di una correzione rapida, completamento o refactoring? Kilo userà il contesto circostante per offrire miglioramenti immediati, mantenendoti nel flusso. Modifica scorciatoia" }, - "keybindingNotFound": "non trovato" + "keybindingNotFound": "non trovato", + "noModelConfigured": "Nessun modello di autocompletamento adatto trovato. Configura un provider nelle impostazioni API.", + "model": "Modello", + "provider": "Provider" } }, "virtualProvider": { diff --git a/webview-ui/src/i18n/locales/ja/kilocode.json b/webview-ui/src/i18n/locales/ja/kilocode.json index 13bf6dc11f0..d39cce7cf18 100644 --- a/webview-ui/src/i18n/locales/ja/kilocode.json +++ b/webview-ui/src/i18n/locales/ja/kilocode.json @@ -229,7 +229,10 @@ "label": "手動オートコンプリート ({{keybinding}})", "description": "素早い修正、補完、またはリファクタリングが必要?Kiloは周囲のコンテキストを使用して即座の改善を提供し、フローを維持します。ショートカットを見る" }, - "keybindingNotFound": "見つかりません" + "keybindingNotFound": "見つかりません", + "noModelConfigured": "適切なオートコンプリートモデルが見つかりませんでした。API設定でプロバイダーを設定してください。", + "model": "モデル", + "provider": "プロバイダー" } }, "virtualProvider": { diff --git a/webview-ui/src/i18n/locales/ko/kilocode.json b/webview-ui/src/i18n/locales/ko/kilocode.json index 411a96dda1a..e27ad805608 100644 --- a/webview-ui/src/i18n/locales/ko/kilocode.json +++ b/webview-ui/src/i18n/locales/ko/kilocode.json @@ -229,7 +229,10 @@ "label": "수동 자동완성({{keybinding}})", "description": "빠른 수정, 완성 또는 리팩토링이 필요하신가요? Kilo가 주변 컨텍스트를 사용하여 즉각적인 개선사항을 제공하여 플로우를 유지합니다. 단축키 보기" }, - "keybindingNotFound": "찾을 수 없음" + "keybindingNotFound": "찾을 수 없음", + "noModelConfigured": "적합한 자동완성 모델을 찾을 수 없습니다. API 설정에서 제공자를 구성하세요.", + "model": "모델", + "provider": "제공자" } }, "virtualProvider": { diff --git a/webview-ui/src/i18n/locales/nl/kilocode.json b/webview-ui/src/i18n/locales/nl/kilocode.json index 0a094b9caee..de53002333b 100644 --- a/webview-ui/src/i18n/locales/nl/kilocode.json +++ b/webview-ui/src/i18n/locales/nl/kilocode.json @@ -229,7 +229,10 @@ "label": "Handmatige Automatische Aanvulling ({{keybinding}})", "description": "Heb je een snelle fix, voltooiing of refactor nodig? Kilo zal de omringende context gebruiken om onmiddellijke verbeteringen aan te bieden, waardoor je in de flow blijft. Sneltoets bewerken" }, - "keybindingNotFound": "niet gevonden" + "keybindingNotFound": "niet gevonden", + "noModelConfigured": "Geen geschikt autocomplete-model gevonden. Configureer een provider in de API-instellingen.", + "model": "Model", + "provider": "Provider" } }, "virtualProvider": { diff --git a/webview-ui/src/i18n/locales/pl/kilocode.json b/webview-ui/src/i18n/locales/pl/kilocode.json index b0ad0e7e948..2cf988a41e7 100644 --- a/webview-ui/src/i18n/locales/pl/kilocode.json +++ b/webview-ui/src/i18n/locales/pl/kilocode.json @@ -222,7 +222,10 @@ "label": "Ręczne uzupełnianie ({{keybinding}})", "description": "Potrzebujesz szybkiej poprawki, uzupełnienia lub refaktoryzacji? Kilo użyje otaczającego kontekstu, aby zaoferować natychmiastowe ulepszenia, utrzymując cię w przepływie. Zobacz skrót" }, - "keybindingNotFound": "nie znaleziono" + "keybindingNotFound": "nie znaleziono", + "noModelConfigured": "Nie znaleziono odpowiedniego modelu autouzupełniania. Skonfiguruj dostawcę w ustawieniach API.", + "model": "Model", + "provider": "Dostawca" } }, "virtualProvider": { diff --git a/webview-ui/src/i18n/locales/pt-BR/kilocode.json b/webview-ui/src/i18n/locales/pt-BR/kilocode.json index cc6990924fb..019b484d0db 100644 --- a/webview-ui/src/i18n/locales/pt-BR/kilocode.json +++ b/webview-ui/src/i18n/locales/pt-BR/kilocode.json @@ -225,7 +225,10 @@ "label": "Preenchimento Automático Manual ({{keybinding}})", "description": "Precisa de uma correção rápida, completação ou refatoração? O Kilo usará o contexto ao redor para oferecer melhorias imediatas, mantendo você no fluxo. Ver atalho" }, - "keybindingNotFound": "não encontrado" + "keybindingNotFound": "não encontrado", + "noModelConfigured": "Nenhum modelo de autocompletar adequado encontrado. Configure um provedor nas configurações da API.", + "model": "Modelo", + "provider": "Provedor" } }, "virtualProvider": { diff --git a/webview-ui/src/i18n/locales/ru/kilocode.json b/webview-ui/src/i18n/locales/ru/kilocode.json index 4d52b893ef6..b8ac0f0f1ad 100644 --- a/webview-ui/src/i18n/locales/ru/kilocode.json +++ b/webview-ui/src/i18n/locales/ru/kilocode.json @@ -225,7 +225,10 @@ "label": "Автодополнение вручную ({{keybinding}})", "description": "Нужно быстрое исправление, дополнение или рефакторинг? Kilo использует окружающий контекст для немедленных улучшений, сохраняя тебя в потоке. Посмотреть горячую клавишу" }, - "keybindingNotFound": "не найдено" + "keybindingNotFound": "не найдено", + "noModelConfigured": "Подходящая модель автодополнения не найдена. Настрой провайдера в настройках API.", + "model": "Модель", + "provider": "Провайдер" } }, "virtualProvider": { diff --git a/webview-ui/src/i18n/locales/th/kilocode.json b/webview-ui/src/i18n/locales/th/kilocode.json index 5c967f7948b..7ccec0192f2 100644 --- a/webview-ui/src/i18n/locales/th/kilocode.json +++ b/webview-ui/src/i18n/locales/th/kilocode.json @@ -229,7 +229,10 @@ "label": "การเติมอัตโนมัติด้วยตัวเอง ({{keybinding}})", "description": "ต้องการการแก้ไขด่วน การเติมเต็ม หรือการปรับโครงสร้างใหม่? Kilo จะใช้บริบทโดยรอบเพื่อเสนอการปรับปรุงทันที ทำให้คุณอยู่ในขั้นตอนการทำงาน ดูทางลัด" }, - "keybindingNotFound": "ไม่พบ" + "keybindingNotFound": "ไม่พบ", + "noModelConfigured": "ไม่พบโมเดลเติมข้อความอัตโนมัติที่เหมาะสม กรุณาตั้งค่าผู้ให้บริการในการตั้งค่า API", + "model": "โมเดล", + "provider": "ผู้ให้บริการ" } }, "virtualProvider": { diff --git a/webview-ui/src/i18n/locales/tr/kilocode.json b/webview-ui/src/i18n/locales/tr/kilocode.json index a386cd6a555..7794c7a1d3e 100644 --- a/webview-ui/src/i18n/locales/tr/kilocode.json +++ b/webview-ui/src/i18n/locales/tr/kilocode.json @@ -222,7 +222,10 @@ "label": "Manuel Otomatik Tamamlama ({{keybinding}})", "description": "Hızlı bir düzeltme, tamamlama veya yeniden düzenleme mi gerekiyor? Kilo çevredeki bağlamı kullanarak anında iyileştirmeler sunacak, seni akışta tutacak. Kısayolu gör" }, - "keybindingNotFound": "bulunamadı" + "keybindingNotFound": "bulunamadı", + "noModelConfigured": "Uygun otomatik tamamlama modeli bulunamadı. Lütfen API ayarlarında bir sağlayıcı yapılandır.", + "model": "Model", + "provider": "Sağlayıcı" } }, "virtualProvider": { diff --git a/webview-ui/src/i18n/locales/uk/kilocode.json b/webview-ui/src/i18n/locales/uk/kilocode.json index a0aa6d229f5..1c84801eaee 100644 --- a/webview-ui/src/i18n/locales/uk/kilocode.json +++ b/webview-ui/src/i18n/locales/uk/kilocode.json @@ -229,7 +229,10 @@ "label": "Ручне автозаповнення ({{keybinding}})", "description": "Потрібне швидке виправлення, доповнення або рефакторинг? Kilo використає навколишній контекст для миттєвих покращень, зберігаючи тебе в потоці. Подивитися гарячу клавішу" }, - "keybindingNotFound": "не знайдено" + "keybindingNotFound": "не знайдено", + "noModelConfigured": "Не знайдено відповідної моделі автодоповнення. Налаштуй провайдера в налаштуваннях API.", + "model": "Модель", + "provider": "Провайдер" } }, "virtualProvider": { diff --git a/webview-ui/src/i18n/locales/vi/kilocode.json b/webview-ui/src/i18n/locales/vi/kilocode.json index 03934df653b..7d664fea906 100644 --- a/webview-ui/src/i18n/locales/vi/kilocode.json +++ b/webview-ui/src/i18n/locales/vi/kilocode.json @@ -225,7 +225,10 @@ "label": "Tự động hoàn thành thủ công ({{keybinding}})", "description": "Cần sửa chữa nhanh, hoàn thành, hoặc tái cấu trúc? Kilo sẽ sử dụng ngữ cảnh xung quanh để cung cấp cải tiến ngay lập tức, giữ bạn trong luồng làm việc. Xem phím tắt" }, - "keybindingNotFound": "không tìm thấy" + "keybindingNotFound": "không tìm thấy", + "noModelConfigured": "Không tìm thấy mô hình tự động hoàn thành phù hợp. Vui lòng cấu hình nhà cung cấp trong cài đặt API.", + "model": "Mô hình", + "provider": "Nhà cung cấp" } }, "virtualProvider": { diff --git a/webview-ui/src/i18n/locales/zh-CN/kilocode.json b/webview-ui/src/i18n/locales/zh-CN/kilocode.json index 3a0aa3179be..78f26bb92ab 100644 --- a/webview-ui/src/i18n/locales/zh-CN/kilocode.json +++ b/webview-ui/src/i18n/locales/zh-CN/kilocode.json @@ -229,7 +229,10 @@ "label": "手动自动完成({{keybinding}})", "description": "需要快速修复、补全或重构?Kilo 将使用周围上下文提供即时改进,保持你的工作流。查看快捷键" }, - "keybindingNotFound": "未找到" + "keybindingNotFound": "未找到", + "noModelConfigured": "未找到合适的自动补全模型。请在 API 设置中配置提供商。", + "model": "模型", + "provider": "Provider" } }, "virtualProvider": { diff --git a/webview-ui/src/i18n/locales/zh-TW/kilocode.json b/webview-ui/src/i18n/locales/zh-TW/kilocode.json index c6ff79cf0df..91e89b394fd 100644 --- a/webview-ui/src/i18n/locales/zh-TW/kilocode.json +++ b/webview-ui/src/i18n/locales/zh-TW/kilocode.json @@ -224,7 +224,10 @@ "label": "手动自动补全 ({{keybinding}})", "description": "需要快速修正、補全或重構?Kilo 將使用周圍內容提供即時改進,保持你的工作流程。檢視快速鍵" }, - "keybindingNotFound": "未找到" + "keybindingNotFound": "未找到", + "noModelConfigured": "找不到合適的自動完成模型。請在 API 設定中配置提供者。", + "model": "模型", + "provider": "Provider" } }, "virtualProvider": {