diff --git a/CHANGELOG.md b/CHANGELOG.md index 505c7fea8fb..ec2be718a81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Docs: https://docs.openclaw.ai ### Changes +- Providers/Kilo Gateway: add first-class `kilocode` provider support (auth, onboarding, implicit provider detection, model defaults, transcript/cache-ttl handling, and docs), with default model `kilocode/anthropic/claude-opus-4.6`. (#20212) Thanks @jrf0110 and @markijbema. - Providers/Vercel AI Gateway: accept Claude shorthand model refs (`vercel-ai-gateway/claude-*`) by normalizing to canonical Anthropic-routed model ids. (#23985) Thanks @sallyom, @markbooch, and @vincentkoc. - Docs/Prompt caching: add a dedicated prompt-caching reference covering `cacheRetention`, per-agent `params` merge precedence, Bedrock/OpenRouter behavior, and cache-ttl + heartbeat tuning. Thanks @svenssonaxel. - Gateway/HTTP security headers: add optional `gateway.http.securityHeaders.strictTransportSecurity` support to emit `Strict-Transport-Security` for direct HTTPS deployments, with runtime wiring, validation, tests, and hardening docs. diff --git a/src/agents/models-config.providers.ts b/src/agents/models-config.providers.ts index 5dc34c52fa4..497b254f8be 100644 --- a/src/agents/models-config.providers.ts +++ b/src/agents/models-config.providers.ts @@ -5,6 +5,14 @@ import { DEFAULT_COPILOT_API_BASE_URL, resolveCopilotApiToken, } from "../providers/github-copilot-token.js"; +import { + KILOCODE_BASE_URL, + KILOCODE_DEFAULT_CONTEXT_WINDOW, + KILOCODE_DEFAULT_COST, + KILOCODE_DEFAULT_MAX_TOKENS, + KILOCODE_DEFAULT_MODEL_ID, + KILOCODE_DEFAULT_MODEL_NAME, +} from "../providers/kilocode-shared.js"; import { ensureAuthProfileStore, listProfilesForProvider } from "./auth-profiles.js"; import { discoverBedrockModels } from "./bedrock-discovery.js"; import { @@ -764,18 +772,6 @@ export function buildNvidiaProvider(): ProviderConfig { }; } -// Kilo Gateway provider -const KILOCODE_BASE_URL = "https://api.kilo.ai/api/gateway/"; -const KILOCODE_DEFAULT_MODEL_ID = "anthropic/claude-opus-4.6"; -const KILOCODE_DEFAULT_CONTEXT_WINDOW = 200000; -const KILOCODE_DEFAULT_MAX_TOKENS = 8192; -const KILOCODE_DEFAULT_COST = { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, -}; - export function buildKilocodeProvider(): ProviderConfig { return { baseUrl: KILOCODE_BASE_URL, @@ -783,7 +779,7 @@ export function buildKilocodeProvider(): ProviderConfig { models: [ { id: KILOCODE_DEFAULT_MODEL_ID, - name: "Claude Opus 4.6", + name: KILOCODE_DEFAULT_MODEL_NAME, reasoning: true, input: ["text", "image"], cost: KILOCODE_DEFAULT_COST, diff --git a/src/commands/auth-choice-options.ts b/src/commands/auth-choice-options.ts index f611be1ce0d..43ef7c4eda0 100644 --- a/src/commands/auth-choice-options.ts +++ b/src/commands/auth-choice-options.ts @@ -1,5 +1,6 @@ import type { AuthProfileStore } from "../agents/auth-profiles.js"; import { AUTH_CHOICE_LEGACY_ALIASES_FOR_CLI } from "./auth-choice-legacy.js"; +import { ONBOARD_PROVIDER_AUTH_FLAGS } from "./onboard-provider-auth-flags.js"; import type { AuthChoice, AuthChoiceGroupId } from "./onboard-types.js"; export type { AuthChoiceGroupId }; @@ -186,6 +187,31 @@ const AUTH_CHOICE_GROUP_DEFS: { }, ]; +const PROVIDER_AUTH_CHOICE_OPTION_HINTS: Partial> = { + "litellm-api-key": "Unified gateway for 100+ LLM providers", + "cloudflare-ai-gateway-api-key": "Account ID + Gateway ID + API key", + "venice-api-key": "Privacy-focused inference (uncensored models)", + "together-api-key": "Access to Llama, DeepSeek, Qwen, and more open models", + "huggingface-api-key": "Inference Providers — OpenAI-compatible chat", +}; + +const PROVIDER_AUTH_CHOICE_OPTION_LABELS: Partial> = { + "moonshot-api-key": "Kimi API key (.ai)", + "moonshot-api-key-cn": "Kimi API key (.cn)", + "kimi-code-api-key": "Kimi Code API key (subscription)", + "cloudflare-ai-gateway-api-key": "Cloudflare AI Gateway", +}; + +function buildProviderAuthChoiceOptions(): AuthChoiceOption[] { + return ONBOARD_PROVIDER_AUTH_FLAGS.map((flag) => ({ + value: flag.authChoice, + label: PROVIDER_AUTH_CHOICE_OPTION_LABELS[flag.authChoice] ?? flag.description, + ...(PROVIDER_AUTH_CHOICE_OPTION_HINTS[flag.authChoice] + ? { hint: PROVIDER_AUTH_CHOICE_OPTION_HINTS[flag.authChoice] } + : {}), + })); +} + const BASE_AUTH_CHOICE_OPTIONS: ReadonlyArray = [ { value: "token", @@ -202,59 +228,11 @@ const BASE_AUTH_CHOICE_OPTIONS: ReadonlyArray = [ label: "vLLM (custom URL + model)", hint: "Local/self-hosted OpenAI-compatible server", }, - { value: "openai-api-key", label: "OpenAI API key" }, - { value: "mistral-api-key", label: "Mistral API key" }, - { value: "xai-api-key", label: "xAI (Grok) API key" }, - { value: "volcengine-api-key", label: "Volcano Engine API key" }, - { value: "byteplus-api-key", label: "BytePlus API key" }, - { - value: "qianfan-api-key", - label: "Qianfan API key", - }, - { value: "openrouter-api-key", label: "OpenRouter API key" }, - { value: "kilocode-api-key", label: "Kilo Gateway API key" }, - { - value: "litellm-api-key", - label: "LiteLLM API key", - hint: "Unified gateway for 100+ LLM providers", - }, - { - value: "ai-gateway-api-key", - label: "Vercel AI Gateway API key", - }, - { - value: "cloudflare-ai-gateway-api-key", - label: "Cloudflare AI Gateway", - hint: "Account ID + Gateway ID + API key", - }, - { - value: "moonshot-api-key", - label: "Kimi API key (.ai)", - }, + ...buildProviderAuthChoiceOptions(), { value: "moonshot-api-key-cn", label: "Kimi API key (.cn)", }, - { - value: "kimi-code-api-key", - label: "Kimi Code API key (subscription)", - }, - { value: "synthetic-api-key", label: "Synthetic API key" }, - { - value: "venice-api-key", - label: "Venice AI API key", - hint: "Privacy-focused inference (uncensored models)", - }, - { - value: "together-api-key", - label: "Together AI API key", - hint: "Access to Llama, DeepSeek, Qwen, and more open models", - }, - { - value: "huggingface-api-key", - label: "Hugging Face API key (HF token)", - hint: "Inference Providers — OpenAI-compatible chat", - }, { value: "github-copilot", label: "GitHub Copilot (GitHub device login)", diff --git a/src/commands/onboard-auth.config-core.ts b/src/commands/onboard-auth.config-core.ts index 328b12a1dd9..f8b45a68017 100644 --- a/src/commands/onboard-auth.config-core.ts +++ b/src/commands/onboard-auth.config-core.ts @@ -29,6 +29,7 @@ import { } from "../agents/venice-models.js"; import type { OpenClawConfig } from "../config/config.js"; import type { ModelApi } from "../config/types.models.js"; +import { KILOCODE_BASE_URL } from "../providers/kilocode-shared.js"; import { HUGGINGFACE_DEFAULT_MODEL_REF, KILOCODE_DEFAULT_MODEL_REF, @@ -433,7 +434,7 @@ export function applyMistralConfig(cfg: OpenClawConfig): OpenClawConfig { return applyAgentDefaultModelPrimary(next, MISTRAL_DEFAULT_MODEL_REF); } -export const KILOCODE_BASE_URL = "https://api.kilo.ai/api/gateway/"; +export { KILOCODE_BASE_URL }; /** * Apply Kilo Gateway provider configuration without changing the default model. @@ -477,6 +478,7 @@ export function applyAuthProfileConfig( preferProfileFirst?: boolean; }, ): OpenClawConfig { + const normalizedProvider = params.provider.toLowerCase(); const profiles = { ...cfg.auth?.profiles, [params.profileId]: { @@ -486,8 +488,13 @@ export function applyAuthProfileConfig( }, }; - // Only maintain `auth.order` when the user explicitly configured it. - // Default behavior: no explicit order -> resolveAuthProfileOrder can round-robin by lastUsed. + const configuredProviderProfiles = Object.entries(cfg.auth?.profiles ?? {}) + .filter(([, profile]) => profile.provider.toLowerCase() === normalizedProvider) + .map(([profileId, profile]) => ({ profileId, mode: profile.mode })); + + // Maintain `auth.order` when it already exists. Additionally, if we detect + // mixed auth modes for the same provider (e.g. legacy oauth + newly selected + // api_key), create an explicit order to keep the newly selected profile first. const existingProviderOrder = cfg.auth?.order?.[params.provider]; const preferProfileFirst = params.preferProfileFirst ?? true; const reorderedProviderOrder = @@ -497,6 +504,18 @@ export function applyAuthProfileConfig( ...existingProviderOrder.filter((profileId) => profileId !== params.profileId), ] : existingProviderOrder; + const hasMixedConfiguredModes = configuredProviderProfiles.some( + ({ profileId, mode }) => profileId !== params.profileId && mode !== params.mode, + ); + const derivedProviderOrder = + existingProviderOrder === undefined && preferProfileFirst && hasMixedConfiguredModes + ? [ + params.profileId, + ...configuredProviderProfiles + .map(({ profileId }) => profileId) + .filter((profileId) => profileId !== params.profileId), + ] + : undefined; const order = existingProviderOrder !== undefined ? { @@ -505,7 +524,12 @@ export function applyAuthProfileConfig( ? reorderedProviderOrder : [...(reorderedProviderOrder ?? []), params.profileId], } - : cfg.auth?.order; + : derivedProviderOrder + ? { + ...cfg.auth?.order, + [params.provider]: derivedProviderOrder, + } + : cfg.auth?.order; return { ...cfg, auth: { diff --git a/src/commands/onboard-auth.credentials.ts b/src/commands/onboard-auth.credentials.ts index fcd0fe29cb0..5d003d48bd1 100644 --- a/src/commands/onboard-auth.credentials.ts +++ b/src/commands/onboard-auth.credentials.ts @@ -4,8 +4,10 @@ import type { OAuthCredentials } from "@mariozechner/pi-ai"; import { resolveOpenClawAgentDir } from "../agents/agent-paths.js"; import { upsertAuthProfile } from "../agents/auth-profiles.js"; import { resolveStateDir } from "../config/paths.js"; +import { KILOCODE_DEFAULT_MODEL_REF } from "../providers/kilocode-shared.js"; export { CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF } from "../agents/cloudflare-ai-gateway.js"; export { MISTRAL_DEFAULT_MODEL_REF, XAI_DEFAULT_MODEL_REF } from "./onboard-auth.models.js"; +export { KILOCODE_DEFAULT_MODEL_REF }; const resolveAuthAgentDir = (agentDir?: string) => agentDir ?? resolveOpenClawAgentDir(); @@ -213,7 +215,6 @@ export const HUGGINGFACE_DEFAULT_MODEL_REF = "huggingface/deepseek-ai/DeepSeek-R export const TOGETHER_DEFAULT_MODEL_REF = "together/moonshotai/Kimi-K2.5"; export const LITELLM_DEFAULT_MODEL_REF = "litellm/claude-opus-4-6"; export const VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF = "vercel-ai-gateway/anthropic/claude-opus-4.6"; -export const KILOCODE_DEFAULT_MODEL_REF = "kilocode/anthropic/claude-opus-4.6"; export async function setZaiApiKey(key: string, agentDir?: string) { // Write to resolved agent dir so gateway finds credentials on startup. diff --git a/src/commands/onboard-auth.models.ts b/src/commands/onboard-auth.models.ts index dc4ac9043d3..cd235ef43d9 100644 --- a/src/commands/onboard-auth.models.ts +++ b/src/commands/onboard-auth.models.ts @@ -1,5 +1,18 @@ import { QIANFAN_BASE_URL, QIANFAN_DEFAULT_MODEL_ID } from "../agents/models-config.providers.js"; import type { ModelDefinitionConfig } from "../config/types.js"; +import { + KILOCODE_DEFAULT_CONTEXT_WINDOW, + KILOCODE_DEFAULT_COST, + KILOCODE_DEFAULT_MAX_TOKENS, + KILOCODE_DEFAULT_MODEL_ID, + KILOCODE_DEFAULT_MODEL_NAME, +} from "../providers/kilocode-shared.js"; +export { + KILOCODE_DEFAULT_CONTEXT_WINDOW, + KILOCODE_DEFAULT_COST, + KILOCODE_DEFAULT_MAX_TOKENS, + KILOCODE_DEFAULT_MODEL_ID, +}; export const DEFAULT_MINIMAX_BASE_URL = "https://api.minimax.io/v1"; export const MINIMAX_API_BASE_URL = "https://api.minimax.io/anthropic"; @@ -205,21 +218,10 @@ export function buildXaiModelDefinition(): ModelDefinitionConfig { }; } -// Kilo Gateway model definitions -export const KILOCODE_DEFAULT_MODEL_ID = "anthropic/claude-opus-4.6"; -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.6", + name: KILOCODE_DEFAULT_MODEL_NAME, reasoning: true, input: ["text", "image"], cost: KILOCODE_DEFAULT_COST, diff --git a/src/commands/onboard-auth.test.ts b/src/commands/onboard-auth.test.ts index 91a60c1eac6..e8671fa1a0d 100644 --- a/src/commands/onboard-auth.test.ts +++ b/src/commands/onboard-auth.test.ts @@ -319,6 +319,44 @@ describe("applyAuthProfileConfig", () => { expect(next.auth?.order?.anthropic).toEqual(["anthropic:work", "anthropic:default"]); }); + + it("creates provider order when switching from legacy oauth to api_key without explicit order", () => { + const next = applyAuthProfileConfig( + { + auth: { + profiles: { + "kilocode:legacy": { provider: "kilocode", mode: "oauth" }, + }, + }, + }, + { + profileId: "kilocode:default", + provider: "kilocode", + mode: "api_key", + }, + ); + + expect(next.auth?.order?.kilocode).toEqual(["kilocode:default", "kilocode:legacy"]); + }); + + it("keeps implicit round-robin when no mixed provider modes are present", () => { + const next = applyAuthProfileConfig( + { + auth: { + profiles: { + "kilocode:legacy": { provider: "kilocode", mode: "api_key" }, + }, + }, + }, + { + profileId: "kilocode:default", + provider: "kilocode", + mode: "api_key", + }, + ); + + expect(next.auth?.order).toBeUndefined(); + }); }); describe("applyMinimaxApiConfig", () => { diff --git a/src/providers/kilocode-shared.ts b/src/providers/kilocode-shared.ts new file mode 100644 index 00000000000..ef90edd1b78 --- /dev/null +++ b/src/providers/kilocode-shared.ts @@ -0,0 +1,12 @@ +export const KILOCODE_BASE_URL = "https://api.kilo.ai/api/gateway/"; +export const KILOCODE_DEFAULT_MODEL_ID = "anthropic/claude-opus-4.6"; +export const KILOCODE_DEFAULT_MODEL_REF = `kilocode/${KILOCODE_DEFAULT_MODEL_ID}`; +export const KILOCODE_DEFAULT_MODEL_NAME = "Claude Opus 4.6"; +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, +} as const;