revert: revert KiloCode provider support

This reverts commit cc10485f0c.
This commit is contained in:
Илия 2026-05-25 21:19:56 +03:00 committed by GitHub
parent cc10485f0c
commit 8e0731f47a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
37 changed files with 55 additions and 964 deletions

View file

@ -1,11 +0,0 @@
import type {
CliProviderModelCatalog,
CliProviderModelCatalogItem,
CliProviderModelCatalogSource,
CliProviderModelCatalogStatus,
} from '@shared/types';
export type KilocodeModelCatalogDto = CliProviderModelCatalog;
export type KilocodeModelCatalogItemDto = CliProviderModelCatalogItem;
export type KilocodeModelCatalogSourceDto = CliProviderModelCatalogSource;
export type KilocodeModelCatalogStatusDto = CliProviderModelCatalogStatus;

View file

@ -1,6 +0,0 @@
export type {
KilocodeModelCatalogDto,
KilocodeModelCatalogItemDto,
KilocodeModelCatalogSourceDto,
KilocodeModelCatalogStatusDto,
} from './dto';

View file

@ -1,32 +0,0 @@
import type { KilocodeModelCatalogItemDto } from '../../contracts';
export function createStaticKilocodeModelCatalogModels(): KilocodeModelCatalogItemDto[] {
return [
{
id: 'claude-sonnet-4-5',
launchModel: 'claude-sonnet-4-5',
displayName: 'Claude Sonnet 4.5',
hidden: false,
supportedReasoningEfforts: [],
defaultReasoningEffort: null,
inputModalities: ['text'],
supportsPersonality: false,
isDefault: true,
upgrade: false,
source: 'static-fallback',
},
{
id: 'claude-opus-4-5',
launchModel: 'claude-opus-4-5',
displayName: 'Claude Opus 4.5',
hidden: false,
supportedReasoningEfforts: [],
defaultReasoningEffort: null,
inputModalities: ['text'],
supportsPersonality: false,
isDefault: false,
upgrade: false,
source: 'static-fallback',
},
];
}

View file

@ -1,8 +0,0 @@
export type {
KilocodeModelCatalogDto,
KilocodeModelCatalogItemDto,
KilocodeModelCatalogSourceDto,
KilocodeModelCatalogStatusDto,
} from './contracts';
export type { KilocodeModelCatalogFeatureFacade, KilocodeModelCatalogRequest } from './main';
export { createKilocodeModelCatalogFeature } from './main';

View file

@ -1,186 +0,0 @@
import { createHash } from 'node:crypto';
import { createStaticKilocodeModelCatalogModels } from '../../core/domain/kilocodeModelCatalogFallback';
import { InMemoryKilocodeModelCatalogCache } from '../infrastructure/InMemoryKilocodeModelCatalogCache';
import { KilocodeGatewayClient } from '../infrastructure/KilocodeGatewayClient';
import type { KilocodeModelCatalogDto, KilocodeModelCatalogItemDto } from '../../contracts';
import type { Logger } from '@shared/utils/logger';
type LoggerPort = Pick<Logger, 'warn'>;
const CATALOG_CACHE_TTL_MS = 10 * 60_000;
const CATALOG_STALE_TTL_MS = 24 * 60 * 60_000;
export interface KilocodeModelCatalogRequest {
apiKey?: string | null;
forceRefresh?: boolean;
}
export interface KilocodeModelCatalogFeatureFacade {
getCatalog(options?: KilocodeModelCatalogRequest): Promise<KilocodeModelCatalogDto>;
invalidate(): void;
}
function nowIso(): string {
return new Date().toISOString();
}
function staleAtIso(): string {
return new Date(Date.now() + CATALOG_CACHE_TTL_MS).toISOString();
}
function buildCacheKey(apiKey: string): string {
return `kilocode:${createHash('sha256').update(apiKey).digest('hex')}`;
}
function normalizeGatewayModels(
models: { id: string; displayName: string }[]
): KilocodeModelCatalogItemDto[] {
return models.map((model, index) => ({
id: model.id,
launchModel: model.id,
displayName: model.displayName,
hidden: false,
supportedReasoningEfforts: [],
defaultReasoningEffort: null,
inputModalities: ['text'],
supportsPersonality: false,
isDefault: index === 0,
upgrade: false,
source: 'app-server' as const,
}));
}
function createFallbackCatalog(options: {
message: string;
status?: KilocodeModelCatalogDto['status'];
appServerState: KilocodeModelCatalogDto['diagnostics']['appServerState'];
}): KilocodeModelCatalogDto {
const models = createStaticKilocodeModelCatalogModels();
const defaultModel = models.find((m) => m.isDefault) ?? models[0] ?? null;
return {
schemaVersion: 1,
providerId: 'kilocode',
source: 'static-fallback',
status: options.status ?? 'degraded',
fetchedAt: nowIso(),
staleAt: staleAtIso(),
defaultModelId: defaultModel?.id ?? null,
defaultLaunchModel: defaultModel?.launchModel ?? null,
models,
diagnostics: {
configReadState: 'skipped',
appServerState: options.appServerState,
message: options.message,
code: null,
},
};
}
export function createKilocodeModelCatalogFeature(options: {
logger: LoggerPort;
}): KilocodeModelCatalogFeatureFacade {
const cache = new InMemoryKilocodeModelCatalogCache();
const inFlightRefreshes = new Map<string, Promise<KilocodeModelCatalogDto>>();
const client = new KilocodeGatewayClient();
async function getCatalog(
request: KilocodeModelCatalogRequest = {}
): Promise<KilocodeModelCatalogDto> {
const apiKey = request.apiKey?.trim() || process.env.KILO_API_KEY?.trim() || null;
if (!apiKey) {
return createFallbackCatalog({
message: 'No KiloCode API key configured. Set KILO_API_KEY or configure an API key.',
appServerState: 'runtime-missing',
status: 'unavailable',
});
}
const cacheKey = buildCacheKey(apiKey);
if (request.forceRefresh !== true) {
const cached = cache.get(cacheKey, CATALOG_CACHE_TTL_MS);
if (cached) {
return cached;
}
}
const existing = inFlightRefreshes.get(cacheKey);
if (existing) {
return existing;
}
const refreshPromise = (async (): Promise<KilocodeModelCatalogDto> => {
try {
const gatewayModels = await client.listModels(apiKey);
const models = normalizeGatewayModels(gatewayModels);
if (models.length === 0) {
throw new Error('KiloCode gateway returned no models.');
}
const defaultModel = models[0] ?? null;
const catalog: KilocodeModelCatalogDto = {
schemaVersion: 1,
providerId: 'kilocode',
source: 'app-server',
status: 'ready',
fetchedAt: nowIso(),
staleAt: staleAtIso(),
defaultModelId: defaultModel?.id ?? null,
defaultLaunchModel: defaultModel?.launchModel ?? null,
models,
diagnostics: {
configReadState: 'skipped',
appServerState: 'healthy',
message: null,
code: null,
},
};
cache.set(cacheKey, catalog);
return catalog;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
const stale = cache.getLatest(cacheKey);
if (stale && Date.parse(stale.fetchedAt) + CATALOG_STALE_TTL_MS > Date.now()) {
return {
...stale,
status: 'stale',
diagnostics: {
configReadState: 'skipped',
appServerState: 'degraded',
message,
code: null,
},
};
}
options.logger.warn('KiloCode model catalog refresh failed', { error: message });
return createFallbackCatalog({
message,
appServerState: 'degraded',
});
}
})();
inFlightRefreshes.set(cacheKey, refreshPromise);
try {
return await refreshPromise;
} finally {
if (inFlightRefreshes.get(cacheKey) === refreshPromise) {
inFlightRefreshes.delete(cacheKey);
}
}
}
return {
getCatalog,
invalidate: () => {
cache.clear();
inFlightRefreshes.clear();
},
};
}

View file

@ -1,5 +0,0 @@
export type {
KilocodeModelCatalogFeatureFacade,
KilocodeModelCatalogRequest,
} from './composition/createKilocodeModelCatalogFeature';
export { createKilocodeModelCatalogFeature } from './composition/createKilocodeModelCatalogFeature';

View file

@ -1,37 +0,0 @@
import type { KilocodeModelCatalogDto } from '../../contracts';
interface CacheEntry {
value: KilocodeModelCatalogDto;
observedAt: number;
}
export class InMemoryKilocodeModelCatalogCache {
private readonly entries = new Map<string, CacheEntry>();
get(key: string, maxAgeMs: number): KilocodeModelCatalogDto | null {
const entry = this.entries.get(key);
if (!entry) {
return null;
}
if (Date.now() - entry.observedAt > maxAgeMs) {
return null;
}
return structuredClone(entry.value);
}
getLatest(key: string): KilocodeModelCatalogDto | null {
const entry = this.entries.get(key);
return entry ? structuredClone(entry.value) : null;
}
set(key: string, value: KilocodeModelCatalogDto): void {
this.entries.set(key, {
value: structuredClone(value),
observedAt: Date.now(),
});
}
clear(): void {
this.entries.clear();
}
}

View file

@ -1,101 +0,0 @@
import https from 'node:https';
const GATEWAY_BASE_URL = 'https://api.kilo.ai';
// KiloCode gateway endpoint: https://kilo.ai/docs/gateway/models-and-providers
const MODELS_PATH = '/api/gateway/models';
const REQUEST_TIMEOUT_MS = 8_000;
const ERROR_BODY_PREVIEW_LIMIT = 500;
interface GatewayModelObject {
id?: string;
object?: string;
created?: number;
owned_by?: string;
display_name?: string;
}
interface GatewayModelsResponse {
object?: string;
data?: GatewayModelObject[];
}
export interface KilocodeGatewayModel {
id: string;
displayName: string;
}
function sanitizeErrorBody(body: string): string {
const sanitized = body
.trim()
.replace(/Bearer\s+[A-Za-z0-9._~-]+/gi, 'Bearer [redacted]')
.replace(/sk-[A-Za-z0-9_-]+/g, '[redacted-api-key]');
if (!sanitized) {
return 'empty response body';
}
return sanitized.length > ERROR_BODY_PREVIEW_LIMIT
? `${sanitized.slice(0, ERROR_BODY_PREVIEW_LIMIT)}...`
: sanitized;
}
export class KilocodeGatewayClient {
async listModels(apiKey: string): Promise<KilocodeGatewayModel[]> {
const raw = await this.fetchModels(apiKey);
const items = raw.data ?? [];
return items
.filter(
(item): item is GatewayModelObject & { id: string } =>
typeof item.id === 'string' && item.id.trim().length > 0
)
.map((item) => ({
id: item.id.trim(),
displayName: (item.display_name ?? item.id).trim(),
}));
}
private fetchModels(apiKey: string): Promise<GatewayModelsResponse> {
return new Promise((resolve, reject) => {
const url = new URL(MODELS_PATH, GATEWAY_BASE_URL);
const options: https.RequestOptions = {
hostname: url.hostname,
port: url.port || 443,
path: url.pathname + url.search,
method: 'GET',
headers: {
Authorization: `Bearer ${apiKey}`,
Accept: 'application/json',
},
};
const req = https.request(options, (res) => {
const chunks: Buffer[] = [];
res.on('data', (chunk: Buffer) => chunks.push(chunk));
res.on('end', () => {
const body = Buffer.concat(chunks).toString('utf8');
if (!res.statusCode || res.statusCode < 200 || res.statusCode >= 300) {
reject(
new Error(
`KiloCode gateway responded with HTTP ${res.statusCode}: ${sanitizeErrorBody(body)}`
)
);
return;
}
try {
resolve(JSON.parse(body) as GatewayModelsResponse);
} catch {
reject(
new Error(`KiloCode gateway returned non-JSON response: ${sanitizeErrorBody(body)}`)
);
}
});
res.on('error', reject);
});
req.setTimeout(REQUEST_TIMEOUT_MS, () => {
req.destroy(new Error(`KiloCode gateway request timed out after ${REQUEST_TIMEOUT_MS}ms`));
});
req.on('error', reject);
req.end();
});
}
}

View file

@ -20,7 +20,7 @@ export type MemberWorkSyncActionableWorkPriority =
| 'blocked'
| 'needs_clarification';
export type MemberWorkSyncProviderId = 'anthropic' | 'codex' | 'gemini' | 'opencode' | 'kilocode';
export type MemberWorkSyncProviderId = 'anthropic' | 'codex' | 'gemini' | 'opencode';
export type MemberWorkSyncReviewObligation = 'review_pickup_required' | 'review_in_progress';

View file

@ -384,7 +384,6 @@ const BRAND_ALIASES: Record<string, string> = {
'gitlab-duo': 'gitlab-duo',
'google-vertex': 'google-vertex',
'hugging-face': 'huggingface',
kilocode: 'kilo',
'mistral-ai': 'mistral',
'ollama-cloud': 'ollama-cloud',
'opencode-zen': 'opencode',

View file

@ -1,10 +1,4 @@
export type WorkspaceTrustProvider =
| 'claude'
| 'anthropic'
| 'codex'
| 'gemini'
| 'opencode'
| 'kilocode';
export type WorkspaceTrustProvider = 'claude' | 'anthropic' | 'codex' | 'gemini' | 'opencode';
export type WorkspaceTrustWorkspaceSource =
| 'team-root'

View file

@ -32,10 +32,6 @@ import {
type CodexModelCatalogFeatureFacade,
createCodexModelCatalogFeature,
} from '@features/codex-model-catalog/main';
import {
type KilocodeModelCatalogFeatureFacade,
createKilocodeModelCatalogFeature,
} from '@features/kilocode-model-catalog/main';
import {
createMemberLogStreamFeature,
registerMemberLogStreamIpc,
@ -903,7 +899,6 @@ let updaterService: UpdaterService;
let sshConnectionManager: SshConnectionManager;
let codexAccountFeature: CodexAccountFeatureFacade | null = null;
let codexModelCatalogFeature: CodexModelCatalogFeatureFacade | null = null;
let kilocodeModelCatalogFeature: KilocodeModelCatalogFeatureFacade | null = null;
let recentProjectsFeature: RecentProjectsFeatureFacade;
let runtimeProviderManagementFeature: RuntimeProviderManagementFeatureFacade;
let memberWorkSyncFeature: MemberWorkSyncFeatureFacade | null = null;
@ -2108,10 +2103,6 @@ async function initializeServices(): Promise<void> {
codexAccountFeature,
});
providerConnectionService.setCodexModelCatalogFeature(codexModelCatalogFeature);
kilocodeModelCatalogFeature = createKilocodeModelCatalogFeature({
logger: createLogger('Feature:KilocodeModelCatalog'),
});
providerConnectionService.setKilocodeModelCatalogFeature(kilocodeModelCatalogFeature);
// startProcessHealthPolling() is deferred to after window creation
// (did-finish-load handler) to avoid thread pool contention at startup.
@ -2364,13 +2355,10 @@ async function shutdownServices(): Promise<void> {
await runShutdownStep('skills watcher stop', () => skillsWatcherService?.stopAll());
await runShutdownStep('provider connection feature detach', () => {
providerConnectionService.setCodexModelCatalogFeature(null);
providerConnectionService.setKilocodeModelCatalogFeature(null);
providerConnectionService.setCodexAccountFeature(null);
});
await runShutdownStep('Codex model catalog dispose', () => codexModelCatalogFeature?.dispose());
codexModelCatalogFeature = null;
kilocodeModelCatalogFeature?.invalidate();
kilocodeModelCatalogFeature = null;
await runShutdownStep('Codex account dispose', () => codexAccountFeature?.dispose());
codexAccountFeature = null;
await runShutdownStep('member work sync dispose', () => memberWorkSyncFeature?.dispose());

View file

@ -44,12 +44,7 @@ const cachedStatus = new Map<
>();
let statusCacheGeneration = 0;
const STATUS_CACHE_TTL_MS = 5_000;
const FRONTEND_MULTIMODEL_PROVIDER_IDS = new Set<CliProviderId>([
'anthropic',
'codex',
'opencode',
'kilocode',
]);
const FRONTEND_MULTIMODEL_PROVIDER_IDS = new Set<CliProviderId>(['anthropic', 'codex', 'opencode']);
function isFrontendMultimodelProviderId(providerId: CliProviderId): boolean {
return FRONTEND_MULTIMODEL_PROVIDER_IDS.has(providerId);

View file

@ -45,7 +45,6 @@ import {
type ProviderModelAvailabilityContext,
type ProviderModelAvailabilitySnapshot,
} from '../runtime/CliProviderModelAvailabilityService';
import { providerConnectionService } from '../runtime/ProviderConnectionService';
import { ClaudeBinaryResolver } from '../team/ClaudeBinaryResolver';
import { getCliFlavorUiOptions, getConfiguredCliFlavor } from '../team/cliFlavor';
@ -72,12 +71,7 @@ const GCS_BASE =
'https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases';
const CLI_INSTALLER_PROGRESS_CHANNEL = 'cliInstaller:progress';
const FRONTEND_MULTIMODEL_PROVIDER_IDS: CliProviderId[] = [
'anthropic',
'codex',
'opencode',
'kilocode',
];
const FRONTEND_MULTIMODEL_PROVIDER_IDS: CliProviderId[] = ['anthropic', 'codex', 'opencode'];
const FRONTEND_MULTIMODEL_PROVIDER_ID_SET = new Set<CliProviderId>(
FRONTEND_MULTIMODEL_PROVIDER_IDS
);
@ -92,8 +86,6 @@ function getProviderDisplayName(providerId: CliProviderId): string {
return 'Gemini';
case 'opencode':
return 'OpenCode (200+ models)';
case 'kilocode':
return 'KiloCode';
}
}
@ -927,10 +919,6 @@ export class CliInstallerService {
background: false,
});
if (providerId === 'kilocode') {
return this.resolveKilocodeProviderStatus();
}
const binaryPath = await ClaudeBinaryResolver.resolve();
if (!binaryPath) {
return null;
@ -971,10 +959,6 @@ export class CliInstallerService {
background: false,
});
if (providerId === 'kilocode') {
return this.resolveKilocodeProviderStatus();
}
const binaryPath = await ClaudeBinaryResolver.resolve();
if (!binaryPath) {
return null;
@ -1220,53 +1204,6 @@ export class CliInstallerService {
result.authMethod = null;
}
private async resolveKilocodeProviderStatus(): Promise<CliProviderStatus> {
const baseStatus: CliProviderStatus = {
providerId: 'kilocode',
displayName: 'KiloCode',
supported: false,
authenticated: false,
authMethod: null,
verificationState: 'verified',
modelVerificationState: 'idle',
statusMessage: null,
detailMessage: null,
models: [],
modelAvailability: [],
canLoginFromUi: true,
capabilities: {
teamLaunch: false,
oneShot: false,
extensions: createDefaultCliExtensionCapabilities(),
},
selectedBackendId: null,
resolvedBackendId: null,
availableBackends: [],
externalRuntimeDiagnostics: [],
backend: null,
connection: null,
modelCatalog: null,
runtimeCapabilities: null,
subscriptionRateLimits: null,
};
// enrichProviderStatus checks both the app key store and process.env for KILO_API_KEY
const enriched = await providerConnectionService.enrichProviderStatus(baseStatus);
const hasApiKey = Boolean(enriched.connection?.apiKeyConfigured);
const status: CliProviderStatus = {
...enriched,
supported: hasApiKey,
authenticated: hasApiKey,
authMethod: hasApiKey ? 'api_key' : null,
statusMessage: hasApiKey ? null : 'Configure KILO_API_KEY to use KiloCode.',
capabilities: {
...enriched.capabilities,
teamLaunch: hasApiKey,
},
};
this.updateLatestProviderStatus(status);
return status;
}
private markProvidersDeferred(result: CliInstallationStatus): void {
if (result.flavor !== 'agent_teams_orchestrator') {
return;
@ -1304,38 +1241,13 @@ export class CliInstallerService {
if (result.flavor === 'agent_teams_orchestrator') {
result.authStatusChecking = true;
let statusTarget = result;
const buildFrontendProviders = async (
providersSnapshot: CliProviderStatus[]
): Promise<CliProviderStatus[]> => {
const providersById = new Map(providersSnapshot.map((provider) => [provider.providerId, provider]));
const frontendProviders: CliProviderStatus[] = [];
for (const providerId of FRONTEND_MULTIMODEL_PROVIDER_IDS) {
if (providerId === 'kilocode') {
frontendProviders.push(await this.resolveKilocodeProviderStatus());
continue;
}
const provider = providersById.get(providerId);
if (provider) {
frontendProviders.push(provider);
}
}
return frontendProviders;
};
const applyProviders = async (
providersSnapshot: CliProviderStatus[],
final: boolean
): Promise<void> => {
const applyProviders = (providersSnapshot: CliProviderStatus[], final: boolean): void => {
if (generation !== this.statusGatherGeneration) {
return;
}
const target = statusTarget;
const frontendProviders = await buildFrontendProviders(
filterFrontendMultimodelProviders(providersSnapshot)
);
if (generation !== this.statusGatherGeneration) {
return;
}
const frontendProviders = filterFrontendMultimodelProviders(providersSnapshot);
target.providers = frontendProviders;
target.authLoggedIn = hasFrontendAuthenticatedProvider(frontendProviders);
target.authMethod = getFrontendAuthenticatedProvider(frontendProviders)?.authMethod ?? null;
@ -1348,10 +1260,10 @@ export class CliInstallerService {
const completion = this.multimodelBridgeService
.getProviderStatuses(binaryPath, (providersSnapshot) => {
void applyProviders(providersSnapshot, false);
applyProviders(providersSnapshot, false);
})
.then((providers) => {
return applyProviders(providers, true);
applyProviders(providers, true);
})
.catch((error) => {
if (generation !== this.statusGatherGeneration) {

View file

@ -335,8 +335,6 @@ function getProviderDisplayName(providerId: CliProviderId): string {
return 'Gemini';
case 'opencode':
return 'OpenCode (200+ models)';
case 'kilocode':
return 'KiloCode';
}
}
@ -423,14 +421,15 @@ function mapRuntimeExtensionCapabilities(
const defaults = capabilities
? createDefaultCliExtensionCapabilities()
: createLegacyRuntimeFallbackCliExtensionCapabilities();
const isExternalRuntime = providerId === 'opencode' || providerId === 'kilocode';
const pluginStatus = isExternalRuntime
? 'unsupported'
: (capabilities?.plugins?.status ?? defaults.plugins.status);
const pluginReason = isExternalRuntime
? (capabilities?.plugins?.reason ??
`${getProviderDisplayName(providerId)} does not support plugin management from Agent Teams.`)
: (capabilities?.plugins?.reason ?? defaults.plugins.reason);
const pluginStatus =
providerId === 'opencode'
? 'unsupported'
: (capabilities?.plugins?.status ?? defaults.plugins.status);
const pluginReason =
providerId === 'opencode'
? (capabilities?.plugins?.reason ??
'OpenCode does not support plugin management from Agent Teams.')
: (capabilities?.plugins?.reason ?? defaults.plugins.reason);
return {
plugins: {

View file

@ -23,7 +23,6 @@ import type {
CodexModelCatalogFeatureFacade,
CodexModelCatalogRequest,
} from '@features/codex-model-catalog/main';
import type { KilocodeModelCatalogFeatureFacade } from '@features/kilocode-model-catalog/main';
import type {
CliProviderAuthMode,
CliProviderConnectionInfo,
@ -66,18 +65,12 @@ const PROVIDER_CAPABILITIES: Record<
supportsApiKey: false,
configurableAuthModes: [],
},
kilocode: {
supportsOAuth: false,
supportsApiKey: true,
configurableAuthModes: ['api_key'],
},
};
const PROVIDER_API_KEY_ENV_VARS: Partial<Record<CliProviderId, string>> = {
anthropic: 'ANTHROPIC_API_KEY',
codex: 'OPENAI_API_KEY',
gemini: 'GEMINI_API_KEY',
kilocode: 'KILO_API_KEY',
};
const ANTHROPIC_BASE_URL_ENV_VAR = 'ANTHROPIC_BASE_URL';
@ -371,10 +364,6 @@ export class ProviderConnectionService {
private codexAccountFeature: CodexAccountSnapshotReader | null = null;
private codexModelCatalogFeature: Pick<CodexModelCatalogFeatureFacade, 'getCatalog'> | null =
null;
private kilocodeModelCatalogFeature: Pick<
KilocodeModelCatalogFeatureFacade,
'getCatalog'
> | null = null;
private readonly anthropicApiKeyVerificationCache = new Map<
string,
{ result: AnthropicApiKeyVerificationResult; at: number }
@ -402,12 +391,6 @@ export class ProviderConnectionService {
this.codexModelCatalogFeature = feature;
}
setKilocodeModelCatalogFeature(
feature: Pick<KilocodeModelCatalogFeatureFacade, 'getCatalog'> | null
): void {
this.kilocodeModelCatalogFeature = feature;
}
async getCodexModelCatalog(
request: CodexModelCatalogRequest = {}
): Promise<CodexModelCatalogDto | null> {
@ -435,10 +418,6 @@ export class ProviderConnectionService {
return this.configManager.getConfig().providerConnections.codex.preferredAuthMode;
}
if (providerId === 'kilocode') {
return 'api_key';
}
return null;
}
@ -620,16 +599,6 @@ export class ProviderConnectionService {
return env;
}
if (providerId === 'kilocode') {
const apiKey = await this.resolveProviderApiKeyForEnv(env, 'kilocode', options);
if (apiKey) {
env.KILO_API_KEY = apiKey;
} else if (typeof env.KILO_API_KEY === 'string' && !env.KILO_API_KEY.trim()) {
delete env.KILO_API_KEY;
}
return env;
}
if (providerId !== 'codex') {
return env;
}
@ -676,7 +645,7 @@ export class ProviderConnectionService {
options?: StoredApiKeyAccessOptions
): Promise<NodeJS.ProcessEnv> {
let nextEnv = env;
for (const providerId of ['anthropic', 'codex', 'gemini', 'opencode', 'kilocode'] as const) {
for (const providerId of ['anthropic', 'codex', 'gemini', 'opencode'] as const) {
nextEnv = await this.applyConfiguredConnectionEnv(nextEnv, providerId, undefined, options);
}
return nextEnv;
@ -712,14 +681,6 @@ export class ProviderConnectionService {
return env;
}
if (providerId === 'kilocode') {
const apiKey = await this.resolveProviderApiKeyForEnv(env, 'kilocode', options);
if (apiKey) {
env.KILO_API_KEY = apiKey;
}
return env;
}
if (providerId !== 'codex') {
return env;
}
@ -761,7 +722,7 @@ export class ProviderConnectionService {
options?: StoredApiKeyAccessOptions
): Promise<NodeJS.ProcessEnv> {
let nextEnv = env;
for (const providerId of ['anthropic', 'codex', 'gemini', 'opencode', 'kilocode'] as const) {
for (const providerId of ['anthropic', 'codex', 'gemini', 'opencode'] as const) {
nextEnv = await this.augmentConfiguredConnectionEnv(nextEnv, providerId, undefined, options);
}
return nextEnv;
@ -804,22 +765,6 @@ export class ProviderConnectionService {
);
}
if (providerId === 'kilocode') {
if (typeof env.KILO_API_KEY === 'string' && env.KILO_API_KEY.trim()) {
return null;
}
if (await this.hasStoredApiKey('KILO_API_KEY')) {
return null;
}
if (this.getExternalCredential('kilocode')?.value.trim()) {
return null;
}
return 'KiloCode API key is not configured. Set KILO_API_KEY or add it in Provider Settings.';
}
if (providerId !== 'codex') {
return null;
}
@ -902,13 +847,7 @@ export class ProviderConnectionService {
async getConfiguredConnectionIssues(
env: NodeJS.ProcessEnv,
providerIds: readonly CliProviderId[] = [
'anthropic',
'codex',
'gemini',
'opencode',
'kilocode',
],
providerIds: readonly CliProviderId[] = ['anthropic', 'codex', 'gemini', 'opencode'],
runtimeBackendOverrides?: Partial<Record<CliProviderId, string>>
): Promise<Partial<Record<CliProviderId, string>>> {
const issues: Partial<Record<CliProviderId, string>> = {};
@ -977,10 +916,6 @@ export class ProviderConnectionService {
return this.enrichAnthropicProviderStatus(withConnection);
}
if (provider.providerId === 'kilocode') {
return this.enrichKilocodeProviderStatus(withConnection);
}
if (provider.providerId !== 'codex') {
return withConnection;
}
@ -1045,33 +980,6 @@ export class ProviderConnectionService {
}
}
private async enrichKilocodeProviderStatus(
provider: CliProviderStatus
): Promise<CliProviderStatus> {
if (!this.kilocodeModelCatalogFeature || !provider.connection?.apiKeyConfigured) {
return provider;
}
try {
const catalog = await this.kilocodeModelCatalogFeature.getCatalog({
apiKey: await this.resolveStoredOrExternalProviderApiKey('kilocode'),
});
if (catalog.status === 'unavailable' || catalog.models.length === 0) {
return provider;
}
const models = catalog.models
.filter((m) => !m.hidden)
.map((m) => m.launchModel.trim())
.filter(Boolean);
return {
...provider,
models: models.length > 0 ? models : provider.models,
modelCatalog: catalog,
};
} catch {
return provider;
}
}
private async enrichAnthropicProviderStatus(
provider: CliProviderStatus
): Promise<CliProviderStatus> {
@ -1288,46 +1196,6 @@ export class ProviderConnectionService {
return this.apiKeyService.lookupPreferred(envVarName);
}
private async resolveStoredOrExternalProviderApiKey(
providerId: CliProviderId,
options?: StoredApiKeyAccessOptions
): Promise<string | null> {
const envVarName = PROVIDER_API_KEY_ENV_VARS[providerId];
if (!envVarName) {
return null;
}
const storedKey = await this.lookupStoredApiKeyValue(envVarName, options);
if (storedKey?.value.trim()) {
return storedKey.value.trim();
}
return this.getExternalCredential(providerId)?.value.trim() || null;
}
private async resolveProviderApiKeyForEnv(
env: NodeJS.ProcessEnv,
providerId: CliProviderId,
options?: StoredApiKeyAccessOptions
): Promise<string | null> {
const envVarName = PROVIDER_API_KEY_ENV_VARS[providerId];
if (!envVarName) {
return null;
}
const storedKey = await this.lookupStoredApiKeyValue(envVarName, options);
if (storedKey?.value.trim()) {
return storedKey.value.trim();
}
const existingValue = env[envVarName];
if (typeof existingValue === 'string' && existingValue.trim()) {
return existingValue.trim();
}
return this.getExternalCredential(providerId)?.value.trim() || null;
}
private getConfiguredCodexRuntimeBackend(runtimeBackendOverride?: string | null): 'codex-native' {
if (runtimeBackendOverride === CODEX_NATIVE_BACKEND_ID) {
return runtimeBackendOverride;
@ -1509,16 +1377,6 @@ export class ProviderConnectionService {
}
}
if (providerId === 'kilocode') {
const apiKey = this.getExternalEnvValue('KILO_API_KEY');
if (apiKey) {
return {
label: 'Detected from KILO_API_KEY',
value: apiKey,
};
}
}
return null;
}

View file

@ -103,12 +103,7 @@ export function applyProviderRuntimeEnv(
export function resolveRuntimeProviderId(
providerId: RuntimeEnvProviderId | undefined
): CliProviderId {
if (
providerId === 'codex' ||
providerId === 'gemini' ||
providerId === 'opencode' ||
providerId === 'kilocode'
) {
if (providerId === 'codex' || providerId === 'gemini' || providerId === 'opencode') {
return providerId;
}
@ -116,10 +111,7 @@ export function resolveRuntimeProviderId(
}
export function resolveTeamProviderId(providerId: TeamProviderId | undefined): TeamProviderId {
return providerId === 'codex' ||
providerId === 'gemini' ||
providerId === 'opencode' ||
providerId === 'kilocode'
return providerId === 'codex' || providerId === 'gemini' || providerId === 'opencode'
? providerId
: 'anthropic';
}

View file

@ -1311,8 +1311,6 @@ function getProviderRuntimeFailureLabel(providerId: TeamProviderId): string {
return 'Gemini runtime';
case 'opencode':
return 'OpenCode runtime';
case 'kilocode':
return 'KiloCode runtime';
}
}
@ -16571,17 +16569,6 @@ export class TeamProvisioningService {
? Array.from(new Set(providerModelChecks.map((check) => check.modelId)))
: selectedModelIds;
if (providerId === 'kilocode') {
const kilocodeConnection =
await this.providerConnectionService.getConnectionInfo('kilocode');
if (!kilocodeConnection.apiKeyConfigured) {
blockingMessages.push(
'KiloCode: API key not configured. Set KILO_API_KEY or add it in Provider Settings.'
);
}
continue;
}
if (providerId === 'opencode') {
const adapter = this.getOpenCodeRuntimeAdapter();
if (!adapter) {

View file

@ -14,13 +14,7 @@ import type {
TeamProvisioningSupportDiagnostic,
} from '@shared/types';
export const TEAM_RUNTIME_PROVIDER_IDS = [
'anthropic',
'codex',
'gemini',
'opencode',
'kilocode',
] as const;
export const TEAM_RUNTIME_PROVIDER_IDS = ['anthropic', 'codex', 'gemini', 'opencode'] as const;
export type TeamRuntimeProviderId = (typeof TEAM_RUNTIME_PROVIDER_IDS)[number];

View file

@ -202,7 +202,5 @@ export const ProviderBrandLogo = ({
return <GeminiBrandLogo className={className} />;
case 'opencode':
return <OpenCodeBrandLogo className={className} />;
case 'kilocode':
return <OpenCodeBrandLogo className={className} />;
}
};

View file

@ -443,8 +443,6 @@ function getProviderLabel(providerId: CliProviderId): string {
return 'Gemini';
case 'opencode':
return 'OpenCode (200+ models)';
case 'kilocode':
return 'KiloCode';
}
}

View file

@ -288,7 +288,6 @@ export function getDashboardRateLimitsForProvider(
return getAnthropicDashboardRateLimits(provider);
case 'gemini':
case 'opencode':
case 'kilocode':
return null;
}
}

View file

@ -62,7 +62,7 @@ const ProviderCapabilityCardSkeleton = ({
providerId,
displayName,
}: {
providerId: 'anthropic' | 'codex' | 'gemini' | 'opencode' | 'kilocode';
providerId: 'anthropic' | 'codex' | 'gemini' | 'opencode';
displayName: string;
}): React.JSX.Element => {
const { t } = useAppTranslation('extensions');

View file

@ -68,7 +68,7 @@ import type { CodexRuntimeStatus } from '@features/codex-runtime-installer/contr
import type { CliProviderAuthMode, CliProviderId, CliProviderStatus } from '@shared/types';
import type { ApiKeyEntry } from '@shared/types/extensions';
type ApiKeyProviderId = 'anthropic' | 'codex' | 'gemini' | 'kilocode';
type ApiKeyProviderId = 'anthropic' | 'codex' | 'gemini';
type PendingConnectionAction = 'auto' | 'oauth' | 'chatgpt' | 'api_key' | 'compatible' | null;
interface ConnectionMethodCardOption {
@ -98,7 +98,7 @@ interface Props {
const API_KEY_PROVIDER_CONFIG: Record<
ApiKeyProviderId,
{
envVarName: 'ANTHROPIC_API_KEY' | 'OPENAI_API_KEY' | 'GEMINI_API_KEY' | 'KILO_API_KEY';
envVarName: 'ANTHROPIC_API_KEY' | 'OPENAI_API_KEY' | 'GEMINI_API_KEY';
name: string;
title: string;
description: string;
@ -129,14 +129,6 @@ const API_KEY_PROVIDER_CONFIG: Record<
'Use `GEMINI_API_KEY` for the Gemini API backend. CLI SDK and ADC do not require it.',
placeholder: 'AIza...',
},
kilocode: {
envVarName: 'KILO_API_KEY',
name: 'KiloCode API Key',
title: 'API key',
description:
'Use your KiloCode API key to authenticate with the KiloCode gateway and load available models.',
placeholder: 'kc-...',
},
};
const API_KEY_PROVIDER_TRANSLATION_KEYS = {
@ -159,7 +151,7 @@ const API_KEY_PROVIDER_TRANSLATION_KEYS = {
placeholder: 'providerRuntime.apiKey.providers.gemini.placeholder',
},
} as const satisfies Record<
Exclude<ApiKeyProviderId, 'kilocode'>,
ApiKeyProviderId,
{
name: string;
title: string;
@ -173,12 +165,7 @@ const ANTHROPIC_COMPATIBLE_AUTH_TOKEN_NAME = 'Anthropic-compatible Auth Token';
const FIRST_PARTY_ANTHROPIC_HOSTS = new Set(['api.anthropic.com', 'api-staging.anthropic.com']);
function isApiKeyProviderId(providerId: CliProviderId): providerId is ApiKeyProviderId {
return (
providerId === 'anthropic' ||
providerId === 'codex' ||
providerId === 'gemini' ||
providerId === 'kilocode'
);
return providerId === 'anthropic' || providerId === 'codex' || providerId === 'gemini';
}
function isCodexRuntimeInstalling(
@ -256,8 +243,6 @@ function getConnectionDescription(
return t('providerRuntime.connection.descriptions.gemini');
case 'opencode':
return t('providerRuntime.connection.descriptions.opencode');
case 'kilocode':
return 'KiloCode uses an API key for authentication with the KiloCode gateway.';
}
}
@ -274,8 +259,6 @@ function getRuntimeDescription(
return t('providerRuntime.runtime.descriptions.gemini');
case 'opencode':
return t('providerRuntime.runtime.descriptions.opencode');
case 'kilocode':
return 'KiloCode uses its own managed runtime host. Configure an API key to use the KiloCode gateway.';
}
}
@ -308,10 +291,6 @@ function getAuthModeDescription(
}
}
if (providerId === 'kilocode' && authMode === 'api_key') {
return 'Use a KiloCode API key for gateway access.';
}
return '';
}
@ -1047,10 +1026,9 @@ export const ProviderRuntimeSettingsDialog = ({
? selectedProvider.providerId
: null;
const apiKeyConfig = apiKeyProviderId ? API_KEY_PROVIDER_CONFIG[apiKeyProviderId] : null;
const apiKeyTranslationKeys =
apiKeyProviderId && apiKeyProviderId !== 'kilocode'
? API_KEY_PROVIDER_TRANSLATION_KEYS[apiKeyProviderId]
: null;
const apiKeyTranslationKeys = apiKeyProviderId
? API_KEY_PROVIDER_TRANSLATION_KEYS[apiKeyProviderId]
: null;
const apiKeyDisplayConfig = apiKeyTranslationKeys
? {
title: t(apiKeyTranslationKeys.title),
@ -1058,7 +1036,7 @@ export const ProviderRuntimeSettingsDialog = ({
name: t(apiKeyTranslationKeys.name),
placeholder: t(apiKeyTranslationKeys.placeholder),
}
: apiKeyConfig;
: null;
const showApiKeyForm =
selectedProvider &&
isApiKeyProviderId(selectedProvider.providerId) &&

View file

@ -124,8 +124,6 @@ function getProviderLabel(providerId: CliProviderId): string {
return 'Gemini';
case 'opencode':
return 'OpenCode (200+ models)';
case 'kilocode':
return 'KiloCode';
}
}

View file

@ -411,7 +411,6 @@ export const DateGroupedSessions = memo((): React.JSX.Element => {
codex: 0,
gemini: 0,
opencode: 0,
kilocode: 0,
};
for (const session of searchedSessions) {

View file

@ -65,7 +65,6 @@ const PROVIDER_LABELS: Record<TeamProviderId, string> = {
codex: 'Codex',
gemini: 'Gemini',
opencode: 'OpenCode',
kilocode: 'KiloCode',
};
function getProviderLabel(providerId: TeamProviderId): string {

View file

@ -28,24 +28,14 @@ const CODEX_PROVIDER_INSTALL_REFRESH_ATTEMPTS = 3;
const CODEX_PROVIDER_INSTALL_REFRESH_RETRY_DELAY_MS = 700;
export const MULTIMODEL_PROVIDER_IDS: CliProviderId[] = isGeminiUiFrozen()
? ['anthropic', 'codex', 'opencode', 'kilocode']
: ['anthropic', 'codex', 'gemini', 'opencode', 'kilocode'];
const MULTIMODEL_PROVIDER_HYDRATION_IDS: CliProviderId[] = isGeminiUiFrozen()
? ['anthropic', 'codex', 'opencode']
: ['anthropic', 'codex', 'gemini', 'opencode'];
const MULTIMODEL_PROVIDER_ID_SET = new Set<CliProviderId>(MULTIMODEL_PROVIDER_IDS);
const MULTIMODEL_PROVIDER_HYDRATION_ID_SET = new Set<CliProviderId>(
MULTIMODEL_PROVIDER_HYDRATION_IDS
);
function isActiveMultimodelProviderId(providerId: CliProviderId): boolean {
return MULTIMODEL_PROVIDER_ID_SET.has(providerId);
}
function isHydratableMultimodelProviderId(providerId: CliProviderId): boolean {
return MULTIMODEL_PROVIDER_HYDRATION_ID_SET.has(providerId);
}
export function createLoadingMultimodelCliStatus(): CliInstallationStatus {
const providers: CliProviderStatus[] = MULTIMODEL_PROVIDER_IDS.map((providerId) => ({
providerId,
@ -269,7 +259,7 @@ export function getIncompleteMultimodelProviderIds(
return status.providers
.filter(
(provider) =>
isHydratableMultimodelProviderId(provider.providerId) &&
isActiveMultimodelProviderId(provider.providerId) &&
!isHydratedMultimodelProviderStatus(provider)
)
.map((provider) => provider.providerId);
@ -285,7 +275,7 @@ export function getModelOnlyFallbackProviderIds(
return status.providers
.filter(
(provider) =>
isHydratableMultimodelProviderId(provider.providerId) &&
isActiveMultimodelProviderId(provider.providerId) &&
isModelOnlyFallbackProviderStatus(provider)
)
.map((provider) => provider.providerId);
@ -303,7 +293,7 @@ export function reconcileMultimodelProviderLoading(
const providersById = new Map(
status.providers.map((provider) => [provider.providerId, provider])
);
return MULTIMODEL_PROVIDER_HYDRATION_IDS.reduce<Partial<Record<CliProviderId, boolean>>>(
return MULTIMODEL_PROVIDER_IDS.reduce<Partial<Record<CliProviderId, boolean>>>(
(nextLoading, providerId) => {
const provider = providersById.get(providerId);
return {
@ -575,9 +565,7 @@ function isMultimodelCliStatus(
function hasActiveProviderStatusLoading(
providerLoading: Partial<Record<CliProviderId, boolean>>
): boolean {
return MULTIMODEL_PROVIDER_HYDRATION_IDS.some(
(providerId) => providerLoading[providerId] === true
);
return MULTIMODEL_PROVIDER_IDS.some((providerId) => providerLoading[providerId] === true);
}
function getAuthenticatedProvider(providers: CliProviderStatus[]): CliProviderStatus | null {
@ -616,8 +604,6 @@ function getProviderDisplayName(providerId: CliProviderId): string {
return 'Gemini';
case 'opencode':
return 'OpenCode (200+ models)';
case 'kilocode':
return 'KiloCode';
}
}
@ -771,7 +757,7 @@ export const createCliInstallerSlice: StateCreator<AppState, [], [], CliInstalle
: createLoadingMultimodelCliStatus();
const shouldMarkIncompleteProvidersLoading = hydrateProviders || providerStatusMode === 'defer';
const providerLoading = Object.fromEntries(
MULTIMODEL_PROVIDER_HYDRATION_IDS.map((providerId) => [
MULTIMODEL_PROVIDER_IDS.map((providerId) => [
providerId,
shouldMarkIncompleteProvidersLoading &&
initialStatus.installed &&
@ -822,14 +808,14 @@ export const createCliInstallerSlice: StateCreator<AppState, [], [], CliInstalle
const nextCliStatus = mergeCliStatusPreservingHydratedProviders(state.cliStatus, metadata);
const nextProviderLoading = Object.fromEntries(
MULTIMODEL_PROVIDER_HYDRATION_IDS.map((providerId) => [
MULTIMODEL_PROVIDER_IDS.map((providerId) => [
providerId,
!isHydratedMultimodelProviderStatus(
nextCliStatus.providers.find((provider) => provider.providerId === providerId)
),
])
) as Partial<Record<CliProviderId, boolean>>;
pendingProviderIds = MULTIMODEL_PROVIDER_HYDRATION_IDS.filter(
pendingProviderIds = MULTIMODEL_PROVIDER_IDS.filter(
(providerId) => nextProviderLoading[providerId] === true
);
const nextAuthState = isMultimodelCliStatus(nextCliStatus)
@ -881,7 +867,7 @@ export const createCliInstallerSlice: StateCreator<AppState, [], [], CliInstalle
try {
if (hydrateProviders) {
await Promise.allSettled(
MULTIMODEL_PROVIDER_HYDRATION_IDS.map((providerId) =>
MULTIMODEL_PROVIDER_IDS.map((providerId) =>
get().fetchCliProviderStatus(providerId, {
silent: false,
epoch,
@ -925,7 +911,7 @@ export const createCliInstallerSlice: StateCreator<AppState, [], [], CliInstalle
});
if (status.installed) {
for (const provider of status.providers) {
if (!isHydratableMultimodelProviderId(provider.providerId)) {
if (!isActiveMultimodelProviderId(provider.providerId)) {
continue;
}
void get().fetchCliProviderStatus(provider.providerId, {

View file

@ -42,7 +42,6 @@ const TEAM_PROVIDER_LABELS: Record<SupportedProviderId, string> = {
codex: 'Codex',
gemini: 'Gemini',
opencode: 'OpenCode',
kilocode: 'KiloCode',
};
const ANTHROPIC_ALIAS_LABELS = {
@ -141,7 +140,6 @@ const TEAM_PROVIDER_MODEL_OPTIONS: Record<SupportedProviderId, readonly TeamProv
},
],
opencode: [{ value: '', label: 'Default', badgeLabel: 'Default' }],
kilocode: [{ value: '', label: 'Default', badgeLabel: 'Default' }],
};
const TEAM_PROVIDER_MODEL_ORDER: Record<SupportedProviderId, Map<string, number>> = {
@ -151,9 +149,6 @@ const TEAM_PROVIDER_MODEL_ORDER: Record<SupportedProviderId, Map<string, number>
opencode: new Map(
TEAM_PROVIDER_MODEL_OPTIONS.opencode.map((option, index) => [option.value, index])
),
kilocode: new Map(
TEAM_PROVIDER_MODEL_OPTIONS.kilocode.map((option, index) => [option.value, index])
),
};
function getKnownTeamProviderModelOption(
@ -342,9 +337,6 @@ export function getTeamModelBadgeLabel(
if (providerId === 'opencode') {
return getTeamModelLabel(trimmed) ?? trimmed;
}
if (providerId === 'kilocode') {
return getTeamModelLabel(trimmed) ?? trimmed;
}
return trimmed;
}

View file

@ -33,7 +33,7 @@ export type CliPlatform =
export type CliFlavor = 'claude' | 'agent_teams_orchestrator';
export type CliProviderId = 'anthropic' | 'codex' | 'gemini' | 'opencode' | 'kilocode';
export type CliProviderId = 'anthropic' | 'codex' | 'gemini' | 'opencode';
export type CliProviderAuthMode = 'auto' | 'oauth' | 'chatgpt' | 'api_key';
export const CLI_PROVIDER_STATUS_DEFERRED_MESSAGE = 'Provider status will refresh when needed.';

View file

@ -966,7 +966,7 @@ export interface TeamViewSnapshot {
}
export type EffortLevel = 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max';
export type TeamProviderId = 'anthropic' | 'codex' | 'gemini' | 'opencode' | 'kilocode';
export type TeamProviderId = 'anthropic' | 'codex' | 'gemini' | 'opencode';
export type TeamProviderBackendId =
| 'auto'
| 'adapter'

View file

@ -3,13 +3,7 @@ import { parseOpenCodeQualifiedModelRef } from './opencodeModelRef';
import type { TeamProviderId } from '@shared/types';
export function isTeamProviderId(value: unknown): value is TeamProviderId {
return (
value === 'anthropic' ||
value === 'codex' ||
value === 'gemini' ||
value === 'opencode' ||
value === 'kilocode'
);
return value === 'anthropic' || value === 'codex' || value === 'gemini' || value === 'opencode';
}
export function normalizeOptionalTeamProviderId(value: unknown): TeamProviderId | undefined {
@ -39,15 +33,6 @@ export function inferTeamProviderIdFromModel(
return 'opencode';
}
if (
normalized.startsWith('kilocode/') ||
normalizedWithoutExtendedContextSuffix.startsWith('kilocode/') ||
normalized.startsWith('kilo/') ||
normalizedWithoutExtendedContextSuffix.startsWith('kilo/')
) {
return 'kilocode';
}
if (
normalized.startsWith('gpt-') ||
normalized.startsWith('codex') ||

View file

@ -239,7 +239,6 @@ describe('CliInstallerService', () => {
'anthropic',
'codex',
'opencode',
'kilocode',
]);
expect(openCodeStatus).toMatchObject({
displayName: 'OpenCode (200+ models)',
@ -336,7 +335,6 @@ describe('CliInstallerService', () => {
'anthropic',
'codex',
'opencode',
'kilocode',
]);
expect(status.authLoggedIn).toBe(false);
expect(status.authMethod).toBeNull();
@ -382,7 +380,7 @@ describe('CliInstallerService', () => {
expect(resolveInteractiveShellEnvBestEffortMock).not.toHaveBeenCalled();
expect(status.authStatusChecking).toBe(false);
expect(status.authLoggedIn).toBe(false);
expect(status.providers).toHaveLength(4);
expect(status.providers).toHaveLength(3);
expect(
status.providers.every(
(provider) => provider.statusMessage === 'Provider status will refresh when needed.'
@ -1215,12 +1213,13 @@ describe('CliInstallerService', () => {
createTestProviderStatus('codex', false, null),
createTestProviderStatus('opencode', false, null),
]);
await vi.waitFor(() => {
const latest = service.getLatestStatusSnapshot();
expect(latest?.authStatusChecking).toBe(false);
expect(latest?.authLoggedIn).toBe(true);
expect(latest?.authMethod).toBe('oauth_token');
});
await Promise.resolve();
await Promise.resolve();
const latest = service.getLatestStatusSnapshot();
expect(latest?.authStatusChecking).toBe(false);
expect(latest?.authLoggedIn).toBe(true);
expect(latest?.authMethod).toBe('oauth_token');
expect(status.authStatusChecking).toBe(true);
expect(status.authLoggedIn).toBe(false);
expect(status.providers.every((provider) => provider.statusMessage === 'Checking...')).toBe(

View file

@ -40,7 +40,6 @@ describe('ProviderConnectionService', () => {
const originalAnthropicApiKey = process.env.ANTHROPIC_API_KEY;
const originalAnthropicAuthToken = process.env.ANTHROPIC_AUTH_TOKEN;
const originalAnthropicBaseUrl = process.env.ANTHROPIC_BASE_URL;
const originalKiloApiKey = process.env.KILO_API_KEY;
function createConfig(
authMode: 'auto' | 'oauth' | 'api_key' = 'auto',
@ -137,7 +136,6 @@ describe('ProviderConnectionService', () => {
delete process.env.ANTHROPIC_API_KEY;
delete process.env.ANTHROPIC_AUTH_TOKEN;
delete process.env.ANTHROPIC_BASE_URL;
delete process.env.KILO_API_KEY;
});
afterEach(() => {
@ -170,12 +168,6 @@ describe('ProviderConnectionService', () => {
} else {
process.env.ANTHROPIC_BASE_URL = originalAnthropicBaseUrl;
}
if (originalKiloApiKey === undefined) {
delete process.env.KILO_API_KEY;
} else {
process.env.KILO_API_KEY = originalKiloApiKey;
}
});
it('removes Anthropic environment credentials when OAuth mode is selected', async () => {
@ -647,126 +639,6 @@ describe('ProviderConnectionService', () => {
expect(result.GEMINI_API_KEY).toBe('gemini-stored-key');
});
it('injects stored KiloCode API keys for runtime launches', async () => {
const lookupPreferred = vi.fn().mockResolvedValue({
envVarName: 'KILO_API_KEY',
value: 'kilo-stored-key',
});
const { ProviderConnectionService } =
await import('@main/services/runtime/ProviderConnectionService');
const service = new ProviderConnectionService(
{
lookupPreferred,
} as never,
{
getConfig: () => createConfig('auto'),
} as never
);
const result = await service.applyConfiguredConnectionEnv({}, 'kilocode');
expect(lookupPreferred).toHaveBeenCalledWith('KILO_API_KEY');
expect(result.KILO_API_KEY).toBe('kilo-stored-key');
await expect(service.getConfiguredConnectionIssue(result, 'kilocode')).resolves.toBeNull();
});
it('reports a missing KiloCode API key before runtime launches', async () => {
const { ProviderConnectionService } =
await import('@main/services/runtime/ProviderConnectionService');
const service = new ProviderConnectionService(
{
lookupPreferred: vi.fn().mockResolvedValue(null),
} as never,
{
getConfig: () => createConfig('auto'),
} as never
);
const issue = await service.getConfiguredConnectionIssue({}, 'kilocode');
expect(issue).toContain('KiloCode API key is not configured');
});
it('passes stored KiloCode API keys to catalog hydration', async () => {
const lookupPreferred = vi.fn().mockResolvedValue({
envVarName: 'KILO_API_KEY',
value: 'kilo-stored-key',
});
const getCatalog = vi.fn().mockResolvedValue({
schemaVersion: 1,
providerId: 'kilocode',
source: 'app-server',
status: 'ready',
fetchedAt: '2026-05-25T00:00:00.000Z',
staleAt: '2026-05-25T00:10:00.000Z',
defaultModelId: 'kilo/test',
defaultLaunchModel: 'kilo/test',
models: [
{
id: 'kilo/test',
launchModel: 'kilo/test',
displayName: 'Kilo Test',
hidden: false,
supportedReasoningEfforts: [],
defaultReasoningEffort: null,
inputModalities: ['text'],
supportsPersonality: false,
isDefault: true,
upgrade: false,
source: 'app-server',
},
],
diagnostics: {
configReadState: 'skipped',
appServerState: 'healthy',
message: null,
code: null,
},
});
const { ProviderConnectionService } =
await import('@main/services/runtime/ProviderConnectionService');
const service = new ProviderConnectionService(
{
lookupPreferred,
} as never,
{
getConfig: () => createConfig('auto'),
} as never
);
service.setKilocodeModelCatalogFeature({ getCatalog });
const enriched = await service.enrichProviderStatus({
providerId: 'kilocode',
displayName: 'KiloCode',
supported: true,
authenticated: true,
authMethod: 'api_key',
verificationState: 'verified',
modelVerificationState: 'idle',
statusMessage: null,
models: [],
modelAvailability: [],
canLoginFromUi: true,
capabilities: {
teamLaunch: true,
oneShot: false,
extensions: {
plugins: { supported: false, status: 'unsupported' },
mcp: { supported: false, status: 'unsupported' },
skills: { supported: false, status: 'unsupported' },
apiKeys: { supported: true, status: 'supported' },
},
},
backend: null,
} as never);
expect(getCatalog).toHaveBeenCalledWith({ apiKey: 'kilo-stored-key' });
expect(enriched.models).toEqual(['kilo/test']);
});
it('reports a missing Anthropic API key when api_key mode is selected', async () => {
const { ProviderConnectionService } =
await import('@main/services/runtime/ProviderConnectionService');

View file

@ -2696,7 +2696,7 @@ describe('CLI status visibility during completed install state', () => {
await Promise.resolve();
});
expect(host.textContent).toContain('Providers: 1/4 connected');
expect(host.textContent).toContain('Providers: 1/3 connected');
expect(host.textContent).toContain('5h left');
expect(host.textContent).toContain('1w left');
expect(host.textContent).toContain('resets');

View file

@ -1,12 +1,11 @@
import React, { act } from 'react';
import { createRoot } from 'react-dom/client';
import { createDefaultCliExtensionCapabilities } from '@shared/utils/providerExtensionCapabilities';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { CodexAccountSnapshotDto } from '@features/codex-account/contracts';
import type { CliInstallationStatus } from '@shared/types';
import type { SkillCatalogItem } from '@shared/types/extensions';
import { createDefaultCliExtensionCapabilities } from '@shared/utils/providerExtensionCapabilities';
interface StoreState {
fetchSkillsCatalog: ReturnType<typeof vi.fn>;
@ -560,7 +559,7 @@ describe('SkillsPanel', () => {
});
expect(host.textContent).toContain(
'Shared skills in `.claude`, `.cursor`, and `.agents` are available to Anthropic, Codex, OpenCode (200+ models), and KiloCode.'
'Shared skills in `.claude`, `.cursor`, and `.agents` are available to Anthropic, Codex, and OpenCode (200+ models).'
);
expect(host.textContent).toContain('Codex only');

View file

@ -62,49 +62,6 @@ vi.mock('@sentry/electron/main', () => sentryNoOp);
vi.mock('@sentry/electron/renderer', () => sentryNoOp);
vi.mock('@sentry/react', () => sentryNoOp);
function createInMemoryStorage(): Storage {
const values = new Map<string, string>();
return {
get length() {
return values.size;
},
clear() {
values.clear();
},
getItem(key: string) {
return values.get(key) ?? null;
},
key(index: number) {
return Array.from(values.keys())[index] ?? null;
},
removeItem(key: string) {
values.delete(key);
},
setItem(key: string, value: string) {
values.set(key, String(value));
},
};
}
function hasStorageApi(value: unknown): value is Storage {
return (
typeof value === 'object' &&
value !== null &&
typeof (value as Storage).getItem === 'function' &&
typeof (value as Storage).setItem === 'function' &&
typeof (value as Storage).removeItem === 'function' &&
typeof (value as Storage).clear === 'function'
);
}
if (!hasStorageApi(globalThis.localStorage)) {
Object.defineProperty(globalThis, 'localStorage', {
configurable: true,
value: createInMemoryStorage(),
});
}
// Mock HOME for tests that need a predictable home path. It must be writable:
// some services persist state in best-effort background writes after a test has
// already reset path overrides.