diff --git a/docs/concepts/model-providers.md b/docs/concepts/model-providers.md index ef27fc9e3..2a4035ca8 100644 --- a/docs/concepts/model-providers.md +++ b/docs/concepts/model-providers.md @@ -107,10 +107,22 @@ Moltbot ships with the pi‑ai catalog. These providers require **no** - Example model: `vercel-ai-gateway/anthropic/claude-opus-4.5` - CLI: `moltbot onboard --auth-choice ai-gateway-api-key` +### Kilo Gateway + +- Provider: `kilocode` +- Auth: `KILOCODE_API_KEY` +- Example model: `kilocode/anthropic/claude-opus-4.5` +- CLI: `moltbot onboard --kilocode-api-key ` +- Base URL: `https://api.kilo.ai/api/openrouter/` + +See [/providers/kilocode](/providers/kilocode) for setup details. + ### Other built-in providers - OpenRouter: `openrouter` (`OPENROUTER_API_KEY`) - Example model: `openrouter/anthropic/claude-sonnet-4-5` +- Kilo Gateway: `kilocode` (`KILOCODE_API_KEY`) +- Example model: `kilocode/anthropic/claude-opus-4.5` - xAI: `xai` (`XAI_API_KEY`) - Groq: `groq` (`GROQ_API_KEY`) - Cerebras: `cerebras` (`CEREBRAS_API_KEY`) diff --git a/docs/design/kilo-gateway-integration.md b/docs/design/kilo-gateway-integration.md new file mode 100644 index 000000000..de5ad4f45 --- /dev/null +++ b/docs/design/kilo-gateway-integration.md @@ -0,0 +1,524 @@ +# Kilo Gateway Provider Integration Design + +## Overview + +This document outlines the design for integrating "Kilo Gateway" as a first-class provider in Moltbot, modeled after the existing OpenRouter implementation. Kilo Gateway uses an OpenAI-compatible completions API with a different base URL. + +## Design Decisions + +### 1. Provider Naming + +**Recommendation: `kilocode`** + +Rationale: +- Matches the user config example provided (`kilocode` provider key) +- Consistent with existing provider naming patterns (e.g., `openrouter`, `opencode`, `moonshot`) +- Short and memorable +- Avoids confusion with generic "kilo" or "gateway" terms + +Alternative considered: `kilo-gateway` - rejected because hyphenated names are less common in the codebase and `kilocode` is more concise. + +### 2. Default Model Reference + +**Recommendation: `kilocode/anthropic/claude-opus-4.5`** + +Rationale: +- Based on user config example +- Claude Opus 4.5 is a capable default model +- Explicit model selection avoids reliance on auto-routing + +### 3. Base URL Configuration + +**Recommendation: Hardcoded default with config override** + +- **Default Base URL:** `https://api.kilo.ai/api/openrouter/` +- **Configurable:** Yes, via `models.providers.kilocode.baseUrl` + +This matches the pattern used by other providers like Moonshot, Venice, and Synthetic. + +### 4. Model Scanning + +**Recommendation: No dedicated model scanning endpoint initially** + +Rationale: +- Kilo Gateway proxies to OpenRouter, so models are dynamic +- Users can manually configure models in their config +- If Kilo Gateway exposes a `/models` endpoint in the future, scanning can be added + +### 5. Special Handling + +**Recommendation: Inherit OpenRouter behavior for Anthropic models** + +Since Kilo Gateway proxies to OpenRouter, the same special handling should apply: +- Cache TTL eligibility for `anthropic/*` models +- Extra params (cacheControlTtl) for `anthropic/*` models +- Transcript policy follows OpenRouter patterns + +## Files to Modify + +### Core Credential Management + +#### 1. `src/commands/onboard-auth.credentials.ts` + +Add: +```typescript +export const KILOCODE_DEFAULT_MODEL_REF = "kilocode/anthropic/claude-opus-4.5"; + +export async function setKilocodeApiKey(key: string, agentDir?: string) { + upsertAuthProfile({ + profileId: "kilocode:default", + credential: { + type: "api_key", + provider: "kilocode", + key, + }, + agentDir: resolveAuthAgentDir(agentDir), + }); +} +``` + +#### 2. `src/agents/model-auth.ts` + +Add to `envMap` in `resolveEnvApiKey()`: +```typescript +const envMap: Record = { + // ... existing entries + kilocode: "KILOCODE_API_KEY", +}; +``` + +#### 3. `src/config/io.ts` + +Add to `SHELL_ENV_EXPECTED_KEYS`: +```typescript +const SHELL_ENV_EXPECTED_KEYS = [ + // ... existing entries + "KILOCODE_API_KEY", +]; +``` + +### Config Application + +#### 4. `src/commands/onboard-auth.config-core.ts` + +Add new functions: +```typescript +export const KILOCODE_BASE_URL = "https://api.kilo.ai/api/openrouter/"; + +export function applyKilocodeProviderConfig(cfg: MoltbotConfig): MoltbotConfig { + const models = { ...cfg.agents?.defaults?.models }; + models[KILOCODE_DEFAULT_MODEL_REF] = { + ...models[KILOCODE_DEFAULT_MODEL_REF], + alias: models[KILOCODE_DEFAULT_MODEL_REF]?.alias ?? "Kilo Gateway", + }; + + const providers = { ...cfg.models?.providers }; + const existingProvider = providers.kilocode; + const { apiKey: existingApiKey, ...existingProviderRest } = (existingProvider ?? {}) as Record< + string, + unknown + > as { apiKey?: string }; + const resolvedApiKey = typeof existingApiKey === "string" ? existingApiKey : undefined; + const normalizedApiKey = resolvedApiKey?.trim(); + + providers.kilocode = { + ...existingProviderRest, + baseUrl: KILOCODE_BASE_URL, + api: "openai-completions", + ...(normalizedApiKey ? { apiKey: normalizedApiKey } : {}), + }; + + return { + ...cfg, + agents: { + ...cfg.agents, + defaults: { + ...cfg.agents?.defaults, + models, + }, + }, + models: { + mode: cfg.models?.mode ?? "merge", + providers, + }, + }; +} + +export function applyKilocodeConfig(cfg: MoltbotConfig): MoltbotConfig { + const next = applyKilocodeProviderConfig(cfg); + const existingModel = next.agents?.defaults?.model; + return { + ...next, + agents: { + ...next.agents, + defaults: { + ...next.agents?.defaults, + model: { + ...(existingModel && "fallbacks" in (existingModel as Record) + ? { + fallbacks: (existingModel as { fallbacks?: string[] }).fallbacks, + } + : undefined), + primary: KILOCODE_DEFAULT_MODEL_REF, + }, + }, + }, + }; +} +``` + +### Auth Choice System + +#### 5. `src/commands/onboard-types.ts` + +Add to `AuthChoice` type: +```typescript +export type AuthChoice = + // ... existing choices + | "kilocode-api-key" + // ... +``` + +Add to `OnboardOptions`: +```typescript +export type OnboardOptions = { + // ... existing options + kilocodeApiKey?: string; + // ... +}; +``` + +#### 6. `src/commands/auth-choice-options.ts` + +Add to `AuthChoiceGroupId`: +```typescript +export type AuthChoiceGroupId = + // ... existing groups + | "kilocode" + // ... +``` + +Add to `AUTH_CHOICE_GROUP_DEFS`: +```typescript +{ + value: "kilocode", + label: "Kilo Gateway", + hint: "API key (OpenRouter-compatible)", + choices: ["kilocode-api-key"], +}, +``` + +Add to `buildAuthChoiceOptions()`: +```typescript +options.push({ + value: "kilocode-api-key", + label: "Kilo Gateway API key", + hint: "OpenRouter-compatible gateway", +}); +``` + +#### 7. `src/commands/auth-choice.preferred-provider.ts` + +Add mapping: +```typescript +const PREFERRED_PROVIDER_BY_AUTH_CHOICE: Partial> = { + // ... existing mappings + "kilocode-api-key": "kilocode", +}; +``` + +### Auth Choice Application + +#### 8. `src/commands/auth-choice.apply.api-providers.ts` + +Add import: +```typescript +import { + // ... existing imports + applyKilocodeConfig, + applyKilocodeProviderConfig, + KILOCODE_DEFAULT_MODEL_REF, + setKilocodeApiKey, +} from "./onboard-auth.js"; +``` + +Add handling for `kilocode-api-key`: +```typescript +if (authChoice === "kilocode-api-key") { + const store = ensureAuthProfileStore(params.agentDir, { + allowKeychainPrompt: false, + }); + const profileOrder = resolveAuthProfileOrder({ + cfg: nextConfig, + store, + provider: "kilocode", + }); + const existingProfileId = profileOrder.find((profileId) => Boolean(store.profiles[profileId])); + const existingCred = existingProfileId ? store.profiles[existingProfileId] : undefined; + let profileId = "kilocode:default"; + let mode: "api_key" | "oauth" | "token" = "api_key"; + let hasCredential = false; + + if (existingProfileId && existingCred?.type) { + profileId = existingProfileId; + mode = existingCred.type === "oauth" ? "oauth" : existingCred.type === "token" ? "token" : "api_key"; + hasCredential = true; + } + + if (!hasCredential && params.opts?.token && params.opts?.tokenProvider === "kilocode") { + await setKilocodeApiKey(normalizeApiKeyInput(params.opts.token), params.agentDir); + hasCredential = true; + } + + if (!hasCredential) { + const envKey = resolveEnvApiKey("kilocode"); + if (envKey) { + const useExisting = await params.prompter.confirm({ + message: `Use existing KILOCODE_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`, + initialValue: true, + }); + if (useExisting) { + await setKilocodeApiKey(envKey.apiKey, params.agentDir); + hasCredential = true; + } + } + } + + if (!hasCredential) { + const key = await params.prompter.text({ + message: "Enter Kilo Gateway API key", + validate: validateApiKeyInput, + }); + await setKilocodeApiKey(normalizeApiKeyInput(String(key)), params.agentDir); + hasCredential = true; + } + + if (hasCredential) { + nextConfig = applyAuthProfileConfig(nextConfig, { + profileId, + provider: "kilocode", + mode, + }); + } + { + const applied = await applyDefaultModelChoice({ + config: nextConfig, + setDefaultModel: params.setDefaultModel, + defaultModel: KILOCODE_DEFAULT_MODEL_REF, + applyDefaultConfig: applyKilocodeConfig, + applyProviderConfig: applyKilocodeProviderConfig, + noteDefault: KILOCODE_DEFAULT_MODEL_REF, + noteAgentModel, + prompter: params.prompter, + }); + nextConfig = applied.config; + agentModelOverride = applied.agentModelOverride ?? agentModelOverride; + } + return { config: nextConfig, agentModelOverride }; +} +``` + +Also add tokenProvider mapping at the top of the function: +```typescript +if (params.opts.tokenProvider === "kilocode") { + authChoice = "kilocode-api-key"; +} +``` + +### CLI Registration + +#### 9. `src/cli/program/register.onboard.ts` + +Add CLI option: +```typescript +.option("--kilocode-api-key ", "Kilo Gateway API key") +``` + +Add to action handler: +```typescript +kilocodeApiKey: opts.kilocodeApiKey as string | undefined, +``` + +Update auth-choice help text: +```typescript +.option( + "--auth-choice ", + "Auth: setup-token|token|chutes|openai-codex|openai-api-key|openrouter-api-key|kilocode-api-key|ai-gateway-api-key|...", +) +``` + +### Non-Interactive Onboarding + +#### 10. `src/commands/onboard-non-interactive/local/auth-choice.ts` + +Add handling for `kilocode-api-key`: +```typescript +if (authChoice === "kilocode-api-key") { + const resolved = await resolveNonInteractiveApiKey({ + provider: "kilocode", + cfg: baseConfig, + flagValue: opts.kilocodeApiKey, + flagName: "--kilocode-api-key", + envVar: "KILOCODE_API_KEY", + }); + await setKilocodeApiKey(resolved.apiKey, agentDir); + nextConfig = applyAuthProfileConfig(nextConfig, { + profileId: "kilocode:default", + provider: "kilocode", + mode: "api_key", + }); + // ... apply default model +} +``` + +### Export Updates + +#### 11. `src/commands/onboard-auth.ts` + +Add exports: +```typescript +export { + // ... existing exports + applyKilocodeConfig, + applyKilocodeProviderConfig, + KILOCODE_BASE_URL, +} from "./onboard-auth.config-core.js"; + +export { + // ... existing exports + KILOCODE_DEFAULT_MODEL_REF, + setKilocodeApiKey, +} from "./onboard-auth.credentials.js"; +``` + +### Special Handling (Optional) + +#### 12. `src/agents/pi-embedded-runner/cache-ttl.ts` + +Add Kilo Gateway support for Anthropic models: +```typescript +export function isCacheTtlEligibleProvider(provider: string, modelId: string): boolean { + const normalizedProvider = provider.toLowerCase(); + const normalizedModelId = modelId.toLowerCase(); + if (normalizedProvider === "anthropic") return true; + if (normalizedProvider === "openrouter" && normalizedModelId.startsWith("anthropic/")) return true; + if (normalizedProvider === "kilocode" && normalizedModelId.startsWith("anthropic/")) return true; + return false; +} +``` + +#### 13. `src/agents/pi-embedded-runner/extra-params.ts` + +Add Kilo Gateway support: +```typescript +function resolveCacheControlTtl( + extraParams: Record | undefined, + provider: string, + modelId: string, +): CacheControlTtl | undefined { + const raw = extraParams?.cacheControlTtl; + if (raw !== "5m" && raw !== "1h") return undefined; + if (provider === "anthropic") return raw; + if (provider === "openrouter" && modelId.startsWith("anthropic/")) return raw; + if (provider === "kilocode" && modelId.startsWith("anthropic/")) return raw; + return undefined; +} +``` + +#### 14. `src/agents/transcript-policy.ts` + +Add Kilo Gateway handling (similar to OpenRouter): +```typescript +const isKilocodeGemini = + provider === "kilocode" && modelId.toLowerCase().includes("gemini"); + +// Include in needsNonImageSanitize check +const needsNonImageSanitize = isGoogle || isAnthropic || isMistral || isOpenRouterGemini || isKilocodeGemini; +``` + +## Configuration Structure + +### User Config Example + +```json +{ + "models": { + "mode": "merge", + "providers": { + "kilocode": { + "baseUrl": "https://api.kilo.ai/api/openrouter/", + "apiKey": "xxxxx", + "api": "openai-completions", + "models": [ + { "id": "anthropic/claude-opus-4.5", "name": "Anthropic: Claude Opus 4.5" }, + { "id": "minimax/minimax-m2.1:free", "name": "Minimax: Minimax M2.1" } + ] + } + } + } +} +``` + +### Auth Profile Structure + +```json +{ + "profiles": { + "kilocode:default": { + "type": "api_key", + "provider": "kilocode", + "key": "xxxxx" + } + } +} +``` + +## Testing Considerations + +1. **Unit Tests:** + - Test `setKilocodeApiKey()` writes correct profile + - Test `applyKilocodeConfig()` sets correct defaults + - Test `resolveEnvApiKey("kilocode")` returns correct env var + +2. **Integration Tests:** + - Test onboarding flow with `--auth-choice kilocode-api-key` + - Test non-interactive onboarding with `--kilocode-api-key` + - Test model selection with `kilocode/` prefix + +3. **E2E Tests:** + - Test actual API calls through Kilo Gateway (live tests) + +## Migration Notes + +- No migration needed for existing users +- New users can immediately use `kilocode-api-key` auth choice +- Existing manual config with `kilocode` provider will continue to work + +## Future Considerations + +1. **Model Catalog:** If Kilo Gateway exposes a `/models` endpoint, add scanning support similar to `scanOpenRouterModels()` + +2. **OAuth Support:** If Kilo Gateway adds OAuth, extend the auth system accordingly + +3. **Rate Limiting:** Consider adding rate limit handling specific to Kilo Gateway if needed + +4. **Documentation:** Add docs at `docs/providers/kilo-gateway.md` explaining setup and usage + +## Summary of Changes + +| File | Change Type | Description | +|------|-------------|-------------| +| `src/commands/onboard-auth.credentials.ts` | Add | `KILOCODE_DEFAULT_MODEL_REF`, `setKilocodeApiKey()` | +| `src/agents/model-auth.ts` | Modify | Add `kilocode` to `envMap` | +| `src/config/io.ts` | Modify | Add `KILOCODE_API_KEY` to shell env keys | +| `src/commands/onboard-auth.config-core.ts` | Add | `applyKilocodeProviderConfig()`, `applyKilocodeConfig()` | +| `src/commands/onboard-types.ts` | Modify | Add `kilocode-api-key` to `AuthChoice`, add `kilocodeApiKey` to options | +| `src/commands/auth-choice-options.ts` | Modify | Add `kilocode` group and option | +| `src/commands/auth-choice.preferred-provider.ts` | Modify | Add `kilocode-api-key` mapping | +| `src/commands/auth-choice.apply.api-providers.ts` | Modify | Add `kilocode-api-key` handling | +| `src/cli/program/register.onboard.ts` | Modify | Add `--kilocode-api-key` option | +| `src/commands/onboard-non-interactive/local/auth-choice.ts` | Modify | Add non-interactive handling | +| `src/commands/onboard-auth.ts` | Modify | Export new functions | +| `src/agents/pi-embedded-runner/cache-ttl.ts` | Modify | Add kilocode support | +| `src/agents/pi-embedded-runner/extra-params.ts` | Modify | Add kilocode support | +| `src/agents/transcript-policy.ts` | Modify | Add kilocode Gemini handling | diff --git a/docs/providers/kilocode.md b/docs/providers/kilocode.md new file mode 100644 index 000000000..846a462ca --- /dev/null +++ b/docs/providers/kilocode.md @@ -0,0 +1,49 @@ +--- +summary: "Use Kilo Gateway's unified API to access many models in Moltbot" +read_when: + - You want a single API key for many LLMs + - You want to run models via Kilo Gateway in Moltbot +--- +# Kilo Gateway + +Kilo Gateway provides a **unified API** that routes requests to many models behind a single +endpoint and API key. It is OpenAI-compatible, so most OpenAI SDKs work by switching the base URL. + +## Getting an API key + +1. Go to [app.kilo.ai](https://app.kilo.ai) +2. Sign in or create an account +3. Navigate to API Keys and generate a new key + +## CLI setup + +```bash +moltbot onboard --kilocode-api-key +``` + +Or set the environment variable: + +```bash +export KILOCODE_API_KEY="your-api-key" +``` + +## Config snippet + +```json5 +{ + env: { KILOCODE_API_KEY: "sk-..." }, + agents: { + defaults: { + model: { primary: "kilocode/anthropic/claude-opus-4.5" } + } + } +} +``` + +## Notes + +- Model refs are `kilocode//` (e.g., `kilocode/anthropic/claude-opus-4.5`). +- Default model: `kilocode/anthropic/claude-opus-4.5` +- Base URL: `https://api.kilo.ai/api/openrouter/` +- For more model/provider options, see [/concepts/model-providers](/concepts/model-providers). +- Kilo Gateway uses a Bearer token with your API key under the hood. diff --git a/src/agents/model-auth.ts b/src/agents/model-auth.ts index 96e4e4ae6..59891e2fc 100644 --- a/src/agents/model-auth.ts +++ b/src/agents/model-auth.ts @@ -285,6 +285,7 @@ export function resolveEnvApiKey(provider: string): EnvApiKeyResult | null { venice: "VENICE_API_KEY", mistral: "MISTRAL_API_KEY", opencode: "OPENCODE_API_KEY", + kilocode: "KILOCODE_API_KEY", }; const envVar = envMap[normalized]; if (!envVar) return null; diff --git a/src/agents/pi-embedded-runner/cache-ttl.ts b/src/agents/pi-embedded-runner/cache-ttl.ts index deb96d6c9..6d381a115 100644 --- a/src/agents/pi-embedded-runner/cache-ttl.ts +++ b/src/agents/pi-embedded-runner/cache-ttl.ts @@ -14,6 +14,7 @@ export function isCacheTtlEligibleProvider(provider: string, modelId: string): b if (normalizedProvider === "anthropic") return true; if (normalizedProvider === "openrouter" && normalizedModelId.startsWith("anthropic/")) return true; + if (normalizedProvider === "kilocode" && normalizedModelId.startsWith("anthropic/")) return true; return false; } diff --git a/src/agents/pi-embedded-runner/extra-params.ts b/src/agents/pi-embedded-runner/extra-params.ts index c23a0438f..afee7122f 100644 --- a/src/agents/pi-embedded-runner/extra-params.ts +++ b/src/agents/pi-embedded-runner/extra-params.ts @@ -32,6 +32,7 @@ function resolveCacheControlTtl( if (raw !== "5m" && raw !== "1h") return undefined; if (provider === "anthropic") return raw; if (provider === "openrouter" && modelId.startsWith("anthropic/")) return raw; + if (provider === "kilocode" && modelId.startsWith("anthropic/")) return raw; return undefined; } diff --git a/src/agents/transcript-policy.ts b/src/agents/transcript-policy.ts index 3ea06ce88..e0b867712 100644 --- a/src/agents/transcript-policy.ts +++ b/src/agents/transcript-policy.ts @@ -74,7 +74,7 @@ export function resolveTranscriptPolicy(params: { const isOpenAi = isOpenAiProvider(provider) || (!provider && isOpenAiApi(params.modelApi)); const isMistral = isMistralModel({ provider, modelId }); const isOpenRouterGemini = - (provider === "openrouter" || provider === "opencode") && + (provider === "openrouter" || provider === "opencode" || provider === "kilocode") && modelId.toLowerCase().includes("gemini"); const isAntigravityClaudeModel = isAntigravityClaude({ api: params.modelApi, diff --git a/src/cli/program/register.onboard.ts b/src/cli/program/register.onboard.ts index 8f31635f0..b064e36f5 100644 --- a/src/cli/program/register.onboard.ts +++ b/src/cli/program/register.onboard.ts @@ -52,7 +52,7 @@ export function registerOnboardCommand(program: Command) { .option("--mode ", "Wizard mode: local|remote") .option( "--auth-choice ", - "Auth: setup-token|token|chutes|openai-codex|openai-api-key|openrouter-api-key|ai-gateway-api-key|moonshot-api-key|kimi-code-api-key|synthetic-api-key|venice-api-key|gemini-api-key|zai-api-key|apiKey|minimax-api|minimax-api-lightning|opencode-zen|skip", + "Auth: setup-token|token|chutes|openai-codex|openai-api-key|openrouter-api-key|kilocode-api-key|ai-gateway-api-key|moonshot-api-key|kimi-code-api-key|synthetic-api-key|venice-api-key|gemini-api-key|zai-api-key|apiKey|minimax-api|minimax-api-lightning|opencode-zen|skip", ) .option( "--token-provider ", @@ -67,6 +67,7 @@ export function registerOnboardCommand(program: Command) { .option("--anthropic-api-key ", "Anthropic API key") .option("--openai-api-key ", "OpenAI API key") .option("--openrouter-api-key ", "OpenRouter API key") + .option("--kilocode-api-key ", "Kilo Gateway API key") .option("--ai-gateway-api-key ", "Vercel AI Gateway API key") .option("--moonshot-api-key ", "Moonshot API key") .option("--kimi-code-api-key ", "Kimi Code API key") @@ -117,6 +118,7 @@ export function registerOnboardCommand(program: Command) { anthropicApiKey: opts.anthropicApiKey as string | undefined, openaiApiKey: opts.openaiApiKey as string | undefined, openrouterApiKey: opts.openrouterApiKey as string | undefined, + kilocodeApiKey: opts.kilocodeApiKey as string | undefined, aiGatewayApiKey: opts.aiGatewayApiKey as string | undefined, moonshotApiKey: opts.moonshotApiKey as string | undefined, kimiCodeApiKey: opts.kimiCodeApiKey as string | undefined, diff --git a/src/commands/auth-choice-options.ts b/src/commands/auth-choice-options.ts index 6b49ff17b..c46604077 100644 --- a/src/commands/auth-choice-options.ts +++ b/src/commands/auth-choice-options.ts @@ -13,6 +13,7 @@ export type AuthChoiceGroupId = | "google" | "copilot" | "openrouter" + | "kilocode" | "ai-gateway" | "moonshot" | "zai" @@ -89,6 +90,12 @@ const AUTH_CHOICE_GROUP_DEFS: { hint: "API key", choices: ["openrouter-api-key"], }, + { + value: "kilocode", + label: "Kilo Gateway", + hint: "API key (OpenRouter-compatible)", + choices: ["kilocode-api-key"], + }, { value: "ai-gateway", label: "Vercel AI Gateway", @@ -135,6 +142,7 @@ export function buildAuthChoiceOptions(params: { options.push({ value: "chutes", label: "Chutes (OAuth)" }); options.push({ value: "openai-api-key", label: "OpenAI API key" }); options.push({ value: "openrouter-api-key", label: "OpenRouter API key" }); + options.push({ value: "kilocode-api-key", label: "Kilo Gateway API key" }); options.push({ value: "ai-gateway-api-key", label: "Vercel AI Gateway API key", diff --git a/src/commands/auth-choice.apply.api-providers.ts b/src/commands/auth-choice.apply.api-providers.ts index 8be02008b..d5ee7653a 100644 --- a/src/commands/auth-choice.apply.api-providers.ts +++ b/src/commands/auth-choice.apply.api-providers.ts @@ -13,6 +13,8 @@ import { } from "./google-gemini-model-default.js"; import { applyAuthProfileConfig, + applyKilocodeConfig, + applyKilocodeProviderConfig, applyKimiCodeConfig, applyKimiCodeProviderConfig, applyMoonshotConfig, @@ -28,6 +30,7 @@ import { applyVercelAiGatewayConfig, applyVercelAiGatewayProviderConfig, applyZaiConfig, + KILOCODE_DEFAULT_MODEL_REF, KIMI_CODE_MODEL_REF, MOONSHOT_DEFAULT_MODEL_REF, OPENROUTER_DEFAULT_MODEL_REF, @@ -35,6 +38,7 @@ import { VENICE_DEFAULT_MODEL_REF, VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF, setGeminiApiKey, + setKilocodeApiKey, setKimiCodeApiKey, setMoonshotApiKey, setOpencodeZenApiKey, @@ -85,6 +89,8 @@ export async function applyAuthChoiceApiProviders( authChoice = "venice-api-key"; } else if (params.opts.tokenProvider === "opencode") { authChoice = "opencode-zen"; + } else if (params.opts.tokenProvider === "kilocode") { + authChoice = "kilocode-api-key"; } } @@ -166,6 +172,84 @@ export async function applyAuthChoiceApiProviders( return { config: nextConfig, agentModelOverride }; } + if (authChoice === "kilocode-api-key") { + const store = ensureAuthProfileStore(params.agentDir, { + allowKeychainPrompt: false, + }); + const profileOrder = resolveAuthProfileOrder({ + cfg: nextConfig, + store, + provider: "kilocode", + }); + const existingProfileId = profileOrder.find((profileId) => Boolean(store.profiles[profileId])); + const existingCred = existingProfileId ? store.profiles[existingProfileId] : undefined; + let profileId = "kilocode:default"; + let mode: "api_key" | "oauth" | "token" = "api_key"; + let hasCredential = false; + + if (existingProfileId && existingCred?.type) { + profileId = existingProfileId; + mode = + existingCred.type === "oauth" + ? "oauth" + : existingCred.type === "token" + ? "token" + : "api_key"; + hasCredential = true; + } + + if (!hasCredential && params.opts?.token && params.opts?.tokenProvider === "kilocode") { + await setKilocodeApiKey(normalizeApiKeyInput(params.opts.token), params.agentDir); + hasCredential = true; + } + + if (!hasCredential) { + const envKey = resolveEnvApiKey("kilocode"); + if (envKey) { + const useExisting = await params.prompter.confirm({ + message: `Use existing KILOCODE_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`, + initialValue: true, + }); + if (useExisting) { + await setKilocodeApiKey(envKey.apiKey, params.agentDir); + hasCredential = true; + } + } + } + + if (!hasCredential) { + const key = await params.prompter.text({ + message: "Enter Kilo Gateway API key", + validate: validateApiKeyInput, + }); + await setKilocodeApiKey(normalizeApiKeyInput(String(key)), params.agentDir); + hasCredential = true; + } + + if (hasCredential) { + nextConfig = applyAuthProfileConfig(nextConfig, { + profileId, + provider: "kilocode", + mode, + }); + } + { + const applied = await applyDefaultModelChoice({ + config: nextConfig, + setDefaultModel: params.setDefaultModel, + defaultModel: KILOCODE_DEFAULT_MODEL_REF, + applyDefaultConfig: applyKilocodeConfig, + applyProviderConfig: applyKilocodeProviderConfig, + noteDefault: KILOCODE_DEFAULT_MODEL_REF, + noteAgentModel, + prompter: params.prompter, + }); + nextConfig = applied.config; + agentModelOverride = applied.agentModelOverride ?? agentModelOverride; + } + return { config: nextConfig, agentModelOverride }; + } + if (authChoice === "ai-gateway-api-key") { let hasCredential = false; diff --git a/src/commands/auth-choice.preferred-provider.ts b/src/commands/auth-choice.preferred-provider.ts index 6fe26b59a..b417bc68a 100644 --- a/src/commands/auth-choice.preferred-provider.ts +++ b/src/commands/auth-choice.preferred-provider.ts @@ -11,6 +11,7 @@ const PREFERRED_PROVIDER_BY_AUTH_CHOICE: Partial> = { chutes: "chutes", "openai-api-key": "openai", "openrouter-api-key": "openrouter", + "kilocode-api-key": "kilocode", "ai-gateway-api-key": "vercel-ai-gateway", "moonshot-api-key": "moonshot", "kimi-code-api-key": "kimi-code", diff --git a/src/commands/onboard-auth.config-core.ts b/src/commands/onboard-auth.config-core.ts index 921ee01d1..a2689b640 100644 --- a/src/commands/onboard-auth.config-core.ts +++ b/src/commands/onboard-auth.config-core.ts @@ -12,13 +12,16 @@ import { } from "../agents/venice-models.js"; import type { MoltbotConfig } from "../config/config.js"; import { + KILOCODE_DEFAULT_MODEL_REF, OPENROUTER_DEFAULT_MODEL_REF, VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF, ZAI_DEFAULT_MODEL_REF, } from "./onboard-auth.credentials.js"; import { + buildKilocodeModelDefinition, buildKimiCodeModelDefinition, buildMoonshotModelDefinition, + KILOCODE_DEFAULT_MODEL_ID, KIMI_CODE_BASE_URL, KIMI_CODE_MODEL_ID, KIMI_CODE_MODEL_REF, @@ -411,6 +414,82 @@ export function applyVeniceConfig(cfg: MoltbotConfig): MoltbotConfig { }; } +export const KILOCODE_BASE_URL = "https://api.kilo.ai/api/openrouter/"; + +/** + * Apply Kilo Gateway provider configuration without changing the default model. + * Registers Kilo Gateway and sets up the provider, but preserves existing model selection. + */ +export function applyKilocodeProviderConfig(cfg: MoltbotConfig): MoltbotConfig { + const models = { ...cfg.agents?.defaults?.models }; + models[KILOCODE_DEFAULT_MODEL_REF] = { + ...models[KILOCODE_DEFAULT_MODEL_REF], + alias: models[KILOCODE_DEFAULT_MODEL_REF]?.alias ?? "Kilo Gateway", + }; + + const providers = { ...cfg.models?.providers }; + const existingProvider = providers.kilocode; + const existingModels = Array.isArray(existingProvider?.models) ? existingProvider.models : []; + const defaultModel = buildKilocodeModelDefinition(); + const hasDefaultModel = existingModels.some((model) => model.id === KILOCODE_DEFAULT_MODEL_ID); + const mergedModels = hasDefaultModel ? existingModels : [...existingModels, defaultModel]; + const { apiKey: existingApiKey, ...existingProviderRest } = (existingProvider ?? {}) as Record< + string, + unknown + > as { apiKey?: string }; + const resolvedApiKey = typeof existingApiKey === "string" ? existingApiKey : undefined; + const normalizedApiKey = resolvedApiKey?.trim(); + + providers.kilocode = { + ...existingProviderRest, + baseUrl: KILOCODE_BASE_URL, + api: "openai-completions", + ...(normalizedApiKey ? { apiKey: normalizedApiKey } : {}), + models: mergedModels.length > 0 ? mergedModels : [defaultModel], + }; + + return { + ...cfg, + agents: { + ...cfg.agents, + defaults: { + ...cfg.agents?.defaults, + models, + }, + }, + models: { + mode: cfg.models?.mode ?? "merge", + providers, + }, + }; +} + +/** + * Apply Kilo Gateway provider configuration AND set Kilo Gateway as the default model. + * Use this when Kilo Gateway is the primary provider choice during onboarding. + */ +export function applyKilocodeConfig(cfg: MoltbotConfig): MoltbotConfig { + const next = applyKilocodeProviderConfig(cfg); + const existingModel = next.agents?.defaults?.model; + return { + ...next, + agents: { + ...next.agents, + defaults: { + ...next.agents?.defaults, + model: { + ...(existingModel && "fallbacks" in (existingModel as Record) + ? { + fallbacks: (existingModel as { fallbacks?: string[] }).fallbacks, + } + : undefined), + primary: KILOCODE_DEFAULT_MODEL_REF, + }, + }, + }, + }; +} + export function applyAuthProfileConfig( cfg: MoltbotConfig, params: { diff --git a/src/commands/onboard-auth.credentials.ts b/src/commands/onboard-auth.credentials.ts index b2fb58542..c822e711a 100644 --- a/src/commands/onboard-auth.credentials.ts +++ b/src/commands/onboard-auth.credentials.ts @@ -115,6 +115,7 @@ export async function setVeniceApiKey(key: string, agentDir?: string) { export const ZAI_DEFAULT_MODEL_REF = "zai/glm-4.7"; export const OPENROUTER_DEFAULT_MODEL_REF = "openrouter/auto"; export const VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF = "vercel-ai-gateway/anthropic/claude-opus-4.5"; +export const KILOCODE_DEFAULT_MODEL_REF = "kilocode/anthropic/claude-opus-4.5"; export async function setZaiApiKey(key: string, agentDir?: string) { // Write to resolved agent dir so gateway finds credentials on startup. @@ -164,3 +165,15 @@ export async function setOpencodeZenApiKey(key: string, agentDir?: string) { agentDir: resolveAuthAgentDir(agentDir), }); } + +export async function setKilocodeApiKey(key: string, agentDir?: string) { + upsertAuthProfile({ + profileId: "kilocode:default", + credential: { + type: "api_key", + provider: "kilocode", + key, + }, + agentDir: resolveAuthAgentDir(agentDir), + }); +} diff --git a/src/commands/onboard-auth.models.ts b/src/commands/onboard-auth.models.ts index de5a4edaa..4384bef4c 100644 --- a/src/commands/onboard-auth.models.ts +++ b/src/commands/onboard-auth.models.ts @@ -116,3 +116,26 @@ export function buildKimiCodeModelDefinition(): ModelDefinitionConfig { compat: KIMI_CODE_COMPAT, }; } + +// Kilo Gateway model definitions +export const KILOCODE_DEFAULT_MODEL_ID = "anthropic/claude-opus-4.5"; +export const KILOCODE_DEFAULT_CONTEXT_WINDOW = 200000; +export const KILOCODE_DEFAULT_MAX_TOKENS = 8192; +export const KILOCODE_DEFAULT_COST = { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, +}; + +export function buildKilocodeModelDefinition(): ModelDefinitionConfig { + return { + id: KILOCODE_DEFAULT_MODEL_ID, + name: "Claude Opus 4.5", + reasoning: true, + input: ["text", "image"], + cost: KILOCODE_DEFAULT_COST, + contextWindow: KILOCODE_DEFAULT_CONTEXT_WINDOW, + maxTokens: KILOCODE_DEFAULT_MAX_TOKENS, + }; +} diff --git a/src/commands/onboard-auth.ts b/src/commands/onboard-auth.ts index b122d89cf..f71ae43fa 100644 --- a/src/commands/onboard-auth.ts +++ b/src/commands/onboard-auth.ts @@ -5,6 +5,8 @@ export { export { VENICE_DEFAULT_MODEL_ID, VENICE_DEFAULT_MODEL_REF } from "../agents/venice-models.js"; export { applyAuthProfileConfig, + applyKilocodeConfig, + applyKilocodeProviderConfig, applyKimiCodeConfig, applyKimiCodeProviderConfig, applyMoonshotConfig, @@ -18,6 +20,7 @@ export { applyVercelAiGatewayConfig, applyVercelAiGatewayProviderConfig, applyZaiConfig, + KILOCODE_BASE_URL, } from "./onboard-auth.config-core.js"; export { applyMinimaxApiConfig, @@ -33,9 +36,11 @@ export { applyOpencodeZenProviderConfig, } from "./onboard-auth.config-opencode.js"; export { + KILOCODE_DEFAULT_MODEL_REF, OPENROUTER_DEFAULT_MODEL_REF, setAnthropicApiKey, setGeminiApiKey, + setKilocodeApiKey, setKimiCodeApiKey, setMinimaxApiKey, setMoonshotApiKey, @@ -50,11 +55,13 @@ export { ZAI_DEFAULT_MODEL_REF, } from "./onboard-auth.credentials.js"; export { + buildKilocodeModelDefinition, buildKimiCodeModelDefinition, buildMinimaxApiModelDefinition, buildMinimaxModelDefinition, buildMoonshotModelDefinition, DEFAULT_MINIMAX_BASE_URL, + KILOCODE_DEFAULT_MODEL_ID, KIMI_CODE_BASE_URL, KIMI_CODE_MODEL_ID, KIMI_CODE_MODEL_REF, diff --git a/src/commands/onboard-non-interactive/local/auth-choice.ts b/src/commands/onboard-non-interactive/local/auth-choice.ts index 7d952730c..3436bd044 100644 --- a/src/commands/onboard-non-interactive/local/auth-choice.ts +++ b/src/commands/onboard-non-interactive/local/auth-choice.ts @@ -8,6 +8,7 @@ import { buildTokenProfileId, validateAnthropicSetupToken } from "../../auth-tok import { applyGoogleGeminiModelDefault } from "../../google-gemini-model-default.js"; import { applyAuthProfileConfig, + applyKilocodeConfig, applyKimiCodeConfig, applyMinimaxApiConfig, applyMinimaxConfig, @@ -20,6 +21,7 @@ import { applyZaiConfig, setAnthropicApiKey, setGeminiApiKey, + setKilocodeApiKey, setKimiCodeApiKey, setMinimaxApiKey, setMoonshotApiKey, @@ -214,6 +216,25 @@ export async function applyNonInteractiveAuthChoice(params: { return applyOpenrouterConfig(nextConfig); } + if (authChoice === "kilocode-api-key") { + const resolved = await resolveNonInteractiveApiKey({ + provider: "kilocode", + cfg: baseConfig, + flagValue: opts.kilocodeApiKey, + flagName: "--kilocode-api-key", + envVar: "KILOCODE_API_KEY", + runtime, + }); + if (!resolved) return null; + if (resolved.source !== "profile") await setKilocodeApiKey(resolved.key); + nextConfig = applyAuthProfileConfig(nextConfig, { + profileId: "kilocode:default", + provider: "kilocode", + mode: "api_key", + }); + return applyKilocodeConfig(nextConfig); + } + if (authChoice === "ai-gateway-api-key") { const resolved = await resolveNonInteractiveApiKey({ provider: "vercel-ai-gateway", diff --git a/src/commands/onboard-types.ts b/src/commands/onboard-types.ts index aa1d9afe0..81ae474d5 100644 --- a/src/commands/onboard-types.ts +++ b/src/commands/onboard-types.ts @@ -12,6 +12,7 @@ export type AuthChoice = | "openai-codex" | "openai-api-key" | "openrouter-api-key" + | "kilocode-api-key" | "ai-gateway-api-key" | "moonshot-api-key" | "kimi-code-api-key" @@ -62,6 +63,7 @@ export type OnboardOptions = { anthropicApiKey?: string; openaiApiKey?: string; openrouterApiKey?: string; + kilocodeApiKey?: string; aiGatewayApiKey?: string; moonshotApiKey?: string; kimiCodeApiKey?: string; diff --git a/src/config/io.ts b/src/config/io.ts index ef8ffba86..ff7e81e08 100644 --- a/src/config/io.ts +++ b/src/config/io.ts @@ -48,6 +48,7 @@ const SHELL_ENV_EXPECTED_KEYS = [ "AI_GATEWAY_API_KEY", "MINIMAX_API_KEY", "SYNTHETIC_API_KEY", + "KILOCODE_API_KEY", "ELEVENLABS_API_KEY", "TELEGRAM_BOT_TOKEN", "DISCORD_BOT_TOKEN",