diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f886b1832d..d34e9405779 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Security/Commands: enforce sender-only matching for `commands.allowFrom` by blocking conversation-shaped `From` identities (`channel:`, `group:`, `thread:`, `@g.us`) while preserving direct-message fallback when sender fields are missing. Ships in the next npm release. Thanks @jiseoung. +- Config/Kilo Gateway: Kilo provider flow now surfaces an updated list of models. (#24921) thanks @gumadeiras. - Security/Config writes: block reserved prototype keys in account-id normalization and route account config resolution through own-key lookups, hardening `/allowlist` and account-scoped config paths against prototype-chain pollution. - Security/Exec: harden `safeBins` long-option validation by rejecting unknown/ambiguous GNU long-option abbreviations and denying sort filesystem-dependent flags (`--random-source`, `--temporary-directory`, `-T`), closing safe-bin denylist bypasses. Thanks @jiseoung. - Security/Channels: unify dangerous name-matching policy checks (`dangerouslyAllowNameMatching`) across core and extension channels, share mutable-allowlist detectors between `openclaw doctor` and `openclaw security audit`, and scan all configured accounts (not only the default account) in channel security audit findings. diff --git a/docs/concepts/model-providers.md b/docs/concepts/model-providers.md index 192b2fa66fc..6210f592482 100644 --- a/docs/concepts/model-providers.md +++ b/docs/concepts/model-providers.md @@ -133,6 +133,7 @@ OpenClaw ships with the pi‑ai catalog. These providers require **no** - Example model: `kilocode/anthropic/claude-opus-4.6` - CLI: `openclaw onboard --kilocode-api-key ` - Base URL: `https://api.kilo.ai/api/gateway/` +- Expanded built-in catalog includes GLM-5 Free, MiniMax M2.5 Free, GPT-5.2, Gemini 3 Pro Preview, Gemini 3 Flash Preview, Grok Code Fast 1, and Kimi K2.5. See [/providers/kilocode](/providers/kilocode) for setup details. diff --git a/docs/providers/kilocode.md b/docs/providers/kilocode.md index 08dbce7c2ce..146e22932c4 100644 --- a/docs/providers/kilocode.md +++ b/docs/providers/kilocode.md @@ -41,6 +41,20 @@ export KILOCODE_API_KEY="your-api-key" } ``` +## Surfaced model refs + +The built-in Kilo Gateway catalog currently surfaces these model refs: + +- `kilocode/anthropic/claude-opus-4.6` (default) +- `kilocode/z-ai/glm-5:free` +- `kilocode/minimax/minimax-m2.5:free` +- `kilocode/anthropic/claude-sonnet-4.5` +- `kilocode/openai/gpt-5.2` +- `kilocode/google/gemini-3-pro-preview` +- `kilocode/google/gemini-3-flash-preview` +- `kilocode/x-ai/grok-code-fast-1` +- `kilocode/moonshotai/kimi-k2.5` + ## Notes - Model refs are `kilocode//` (e.g., `kilocode/anthropic/claude-opus-4.6`). diff --git a/src/agents/model-catalog.test.ts b/src/agents/model-catalog.test.ts index 791947ad8fa..5d69ae1c4ea 100644 --- a/src/agents/model-catalog.test.ts +++ b/src/agents/model-catalog.test.ts @@ -103,4 +103,124 @@ describe("loadModelCatalog", () => { expect(spark?.name).toBe("gpt-5.3-codex-spark"); expect(spark?.reasoning).toBe(true); }); + + it("merges configured models for opted-in non-pi-native providers", async () => { + __setModelCatalogImportForTest( + async () => + ({ + AuthStorage: class {}, + ModelRegistry: class { + getAll() { + return [{ id: "gpt-4.1", provider: "openai", name: "GPT-4.1" }]; + } + }, + }) as unknown as PiSdkModule, + ); + + const result = await loadModelCatalog({ + config: { + models: { + providers: { + kilocode: { + models: [ + { + id: "google/gemini-3-pro-preview", + name: "Gemini 3 Pro Preview", + input: ["text", "image"], + reasoning: true, + contextWindow: 1048576, + }, + ], + }, + }, + }, + } as OpenClawConfig, + }); + + expect(result).toContainEqual( + expect.objectContaining({ + provider: "kilocode", + id: "google/gemini-3-pro-preview", + name: "Gemini 3 Pro Preview", + }), + ); + }); + + it("does not merge configured models for providers that are not opted in", async () => { + __setModelCatalogImportForTest( + async () => + ({ + AuthStorage: class {}, + ModelRegistry: class { + getAll() { + return [{ id: "gpt-4.1", provider: "openai", name: "GPT-4.1" }]; + } + }, + }) as unknown as PiSdkModule, + ); + + const result = await loadModelCatalog({ + config: { + models: { + providers: { + qianfan: { + models: [ + { + id: "deepseek-v3.2", + name: "DEEPSEEK V3.2", + }, + ], + }, + }, + }, + } as OpenClawConfig, + }); + + expect( + result.some((entry) => entry.provider === "qianfan" && entry.id === "deepseek-v3.2"), + ).toBe(false); + }); + + it("does not duplicate opted-in configured models already present in ModelRegistry", async () => { + __setModelCatalogImportForTest( + async () => + ({ + AuthStorage: class {}, + ModelRegistry: class { + getAll() { + return [ + { + id: "anthropic/claude-opus-4.6", + provider: "kilocode", + name: "Claude Opus 4.6", + }, + ]; + } + }, + }) as unknown as PiSdkModule, + ); + + const result = await loadModelCatalog({ + config: { + models: { + providers: { + kilocode: { + models: [ + { + id: "anthropic/claude-opus-4.6", + name: "Configured Claude Opus 4.6", + }, + ], + }, + }, + }, + } as OpenClawConfig, + }); + + const matches = result.filter( + (entry) => entry.provider === "kilocode" && entry.id === "anthropic/claude-opus-4.6", + ); + expect(matches).toHaveLength(1); + expect(matches[0]?.name).toBe("Claude Opus 4.6"); + }); }); diff --git a/src/agents/model-catalog.ts b/src/agents/model-catalog.ts index beda4dc5848..82ca5686493 100644 --- a/src/agents/model-catalog.ts +++ b/src/agents/model-catalog.ts @@ -33,6 +33,7 @@ let importPiSdk = defaultImportPiSdk; const CODEX_PROVIDER = "openai-codex"; const OPENAI_CODEX_GPT53_MODEL_ID = "gpt-5.3-codex"; const OPENAI_CODEX_GPT53_SPARK_MODEL_ID = "gpt-5.3-codex-spark"; +const NON_PI_NATIVE_MODEL_PROVIDERS = new Set(["kilocode"]); function applyOpenAICodexSparkFallback(models: ModelCatalogEntry[]): void { const hasSpark = models.some( @@ -59,6 +60,89 @@ function applyOpenAICodexSparkFallback(models: ModelCatalogEntry[]): void { }); } +function normalizeConfiguredModelInput(input: unknown): Array<"text" | "image"> | undefined { + if (!Array.isArray(input)) { + return undefined; + } + const normalized = input.filter( + (item): item is "text" | "image" => item === "text" || item === "image", + ); + return normalized.length > 0 ? normalized : undefined; +} + +function readConfiguredOptInProviderModels(config: OpenClawConfig): ModelCatalogEntry[] { + const providers = config.models?.providers; + if (!providers || typeof providers !== "object") { + return []; + } + + const out: ModelCatalogEntry[] = []; + for (const [providerRaw, providerValue] of Object.entries(providers)) { + const provider = providerRaw.toLowerCase().trim(); + if (!NON_PI_NATIVE_MODEL_PROVIDERS.has(provider)) { + continue; + } + if (!providerValue || typeof providerValue !== "object") { + continue; + } + + const configuredModels = (providerValue as { models?: unknown }).models; + if (!Array.isArray(configuredModels)) { + continue; + } + + for (const configuredModel of configuredModels) { + if (!configuredModel || typeof configuredModel !== "object") { + continue; + } + const idRaw = (configuredModel as { id?: unknown }).id; + if (typeof idRaw !== "string") { + continue; + } + const id = idRaw.trim(); + if (!id) { + continue; + } + const rawName = (configuredModel as { name?: unknown }).name; + const name = (typeof rawName === "string" ? rawName : id).trim() || id; + const contextWindowRaw = (configuredModel as { contextWindow?: unknown }).contextWindow; + const contextWindow = + typeof contextWindowRaw === "number" && contextWindowRaw > 0 ? contextWindowRaw : undefined; + const reasoningRaw = (configuredModel as { reasoning?: unknown }).reasoning; + const reasoning = typeof reasoningRaw === "boolean" ? reasoningRaw : undefined; + const input = normalizeConfiguredModelInput((configuredModel as { input?: unknown }).input); + out.push({ id, name, provider, contextWindow, reasoning, input }); + } + } + + return out; +} + +function mergeConfiguredOptInProviderModels(params: { + config: OpenClawConfig; + models: ModelCatalogEntry[]; +}): void { + const configured = readConfiguredOptInProviderModels(params.config); + if (configured.length === 0) { + return; + } + + const seen = new Set( + params.models.map( + (entry) => `${entry.provider.toLowerCase().trim()}::${entry.id.toLowerCase().trim()}`, + ), + ); + + for (const entry of configured) { + const key = `${entry.provider.toLowerCase().trim()}::${entry.id.toLowerCase().trim()}`; + if (seen.has(key)) { + continue; + } + params.models.push(entry); + seen.add(key); + } +} + export function resetModelCatalogCacheForTest() { modelCatalogPromise = null; hasLoggedModelCatalogError = false; @@ -142,6 +226,7 @@ export async function loadModelCatalog(params?: { const input = Array.isArray(entry?.input) ? entry.input : undefined; models.push({ id, name, provider, contextWindow, reasoning, input }); } + mergeConfiguredOptInProviderModels({ config: cfg, models }); applyOpenAICodexSparkFallback(models); if (models.length === 0) { diff --git a/src/agents/models-config.providers.kilocode.test.ts b/src/agents/models-config.providers.kilocode.test.ts index bb709d7d075..05cfb1b468c 100644 --- a/src/agents/models-config.providers.kilocode.test.ts +++ b/src/agents/models-config.providers.kilocode.test.ts @@ -5,6 +5,18 @@ import { describe, expect, it } from "vitest"; import { captureEnv } from "../test-utils/env.js"; import { buildKilocodeProvider, resolveImplicitProviders } from "./models-config.providers.js"; +const KILOCODE_MODEL_IDS = [ + "anthropic/claude-opus-4.6", + "z-ai/glm-5:free", + "minimax/minimax-m2.5:free", + "anthropic/claude-sonnet-4.5", + "openai/gpt-5.2", + "google/gemini-3-pro-preview", + "google/gemini-3-flash-preview", + "x-ai/grok-code-fast-1", + "moonshotai/kimi-k2.5", +]; + describe("Kilo Gateway implicit provider", () => { it("should include kilocode when KILOCODE_API_KEY is configured", async () => { const agentDir = mkdtempSync(join(tmpdir(), "openclaw-test-")); @@ -46,4 +58,12 @@ describe("Kilo Gateway implicit provider", () => { const modelIds = provider.models.map((m) => m.id); expect(modelIds).toContain("anthropic/claude-opus-4.6"); }); + + it("should include the full surfaced model catalog", () => { + const provider = buildKilocodeProvider(); + const modelIds = provider.models.map((m) => m.id); + for (const modelId of KILOCODE_MODEL_IDS) { + expect(modelIds).toContain(modelId); + } + }); }); diff --git a/src/agents/models-config.providers.ts b/src/agents/models-config.providers.ts index 497b254f8be..3662ce9a3b1 100644 --- a/src/agents/models-config.providers.ts +++ b/src/agents/models-config.providers.ts @@ -10,8 +10,7 @@ import { KILOCODE_DEFAULT_CONTEXT_WINDOW, KILOCODE_DEFAULT_COST, KILOCODE_DEFAULT_MAX_TOKENS, - KILOCODE_DEFAULT_MODEL_ID, - KILOCODE_DEFAULT_MODEL_NAME, + KILOCODE_MODEL_CATALOG, } from "../providers/kilocode-shared.js"; import { ensureAuthProfileStore, listProfilesForProvider } from "./auth-profiles.js"; import { discoverBedrockModels } from "./bedrock-discovery.js"; @@ -776,17 +775,15 @@ export function buildKilocodeProvider(): ProviderConfig { return { baseUrl: KILOCODE_BASE_URL, api: "openai-completions", - models: [ - { - id: KILOCODE_DEFAULT_MODEL_ID, - name: KILOCODE_DEFAULT_MODEL_NAME, - reasoning: true, - input: ["text", "image"], - cost: KILOCODE_DEFAULT_COST, - contextWindow: KILOCODE_DEFAULT_CONTEXT_WINDOW, - maxTokens: KILOCODE_DEFAULT_MAX_TOKENS, - }, - ], + models: KILOCODE_MODEL_CATALOG.map((model) => ({ + id: model.id, + name: model.name, + reasoning: model.reasoning, + input: model.input, + cost: KILOCODE_DEFAULT_COST, + contextWindow: model.contextWindow ?? KILOCODE_DEFAULT_CONTEXT_WINDOW, + maxTokens: model.maxTokens ?? KILOCODE_DEFAULT_MAX_TOKENS, + })), }; } diff --git a/src/commands/configure.gateway-auth.prompt-auth-config.test.ts b/src/commands/configure.gateway-auth.prompt-auth-config.test.ts new file mode 100644 index 00000000000..26ebe1e3d73 --- /dev/null +++ b/src/commands/configure.gateway-auth.prompt-auth-config.test.ts @@ -0,0 +1,131 @@ +import { describe, expect, it, vi } from "vitest"; +import type { RuntimeEnv } from "../runtime.js"; +import type { WizardPrompter } from "../wizard/prompts.js"; + +const mocks = vi.hoisted(() => ({ + promptAuthChoiceGrouped: vi.fn(), + applyAuthChoice: vi.fn(), + promptModelAllowlist: vi.fn(), + promptDefaultModel: vi.fn(), + promptCustomApiConfig: vi.fn(), +})); + +vi.mock("../agents/auth-profiles.js", () => ({ + ensureAuthProfileStore: vi.fn(() => ({ + version: 1, + profiles: {}, + })), +})); + +vi.mock("./auth-choice-prompt.js", () => ({ + promptAuthChoiceGrouped: mocks.promptAuthChoiceGrouped, +})); + +vi.mock("./auth-choice.js", () => ({ + applyAuthChoice: mocks.applyAuthChoice, + resolvePreferredProviderForAuthChoice: vi.fn(() => undefined), +})); + +vi.mock("./model-picker.js", async (importActual) => { + const actual = await importActual(); + return { + ...actual, + promptModelAllowlist: mocks.promptModelAllowlist, + promptDefaultModel: mocks.promptDefaultModel, + }; +}); + +vi.mock("./onboard-custom.js", () => ({ + promptCustomApiConfig: mocks.promptCustomApiConfig, +})); + +import { promptAuthConfig } from "./configure.gateway-auth.js"; + +function makeRuntime(): RuntimeEnv { + return { + log: vi.fn(), + error: vi.fn(), + exit: vi.fn(), + }; +} + +const noopPrompter = {} as WizardPrompter; + +describe("promptAuthConfig", () => { + it("prunes Kilo provider models to selected allowlist entries", async () => { + mocks.promptAuthChoiceGrouped.mockResolvedValue("kilocode-api-key"); + mocks.applyAuthChoice.mockResolvedValue({ + config: { + agents: { + defaults: { + model: { primary: "kilocode/anthropic/claude-opus-4.6" }, + }, + }, + models: { + providers: { + kilocode: { + baseUrl: "https://api.kilo.ai/api/gateway/", + api: "openai-completions", + models: [ + { id: "anthropic/claude-opus-4.6", name: "Claude Opus 4.6" }, + { id: "minimax/minimax-m2.5:free", name: "MiniMax M2.5 (Free)" }, + ], + }, + }, + }, + }, + }); + mocks.promptModelAllowlist.mockResolvedValue({ + models: ["kilocode/anthropic/claude-opus-4.6"], + }); + + const result = await promptAuthConfig({}, makeRuntime(), noopPrompter); + expect(result.models?.providers?.kilocode?.models?.map((model) => model.id)).toEqual([ + "anthropic/claude-opus-4.6", + ]); + expect(Object.keys(result.agents?.defaults?.models ?? {})).toEqual([ + "kilocode/anthropic/claude-opus-4.6", + ]); + }); + + it("does not mutate non-Kilo provider models when allowlist contains Kilo entries", async () => { + mocks.promptAuthChoiceGrouped.mockResolvedValue("kilocode-api-key"); + mocks.applyAuthChoice.mockResolvedValue({ + config: { + agents: { + defaults: { + model: { primary: "kilocode/anthropic/claude-opus-4.6" }, + }, + }, + models: { + providers: { + kilocode: { + baseUrl: "https://api.kilo.ai/api/gateway/", + api: "openai-completions", + models: [ + { id: "anthropic/claude-opus-4.6", name: "Claude Opus 4.6" }, + { id: "minimax/minimax-m2.5:free", name: "MiniMax M2.5 (Free)" }, + ], + }, + minimax: { + baseUrl: "https://api.minimax.io/anthropic", + api: "anthropic-messages", + models: [{ id: "MiniMax-M2.1", name: "MiniMax M2.1" }], + }, + }, + }, + }, + }); + mocks.promptModelAllowlist.mockResolvedValue({ + models: ["kilocode/anthropic/claude-opus-4.6"], + }); + + const result = await promptAuthConfig({}, makeRuntime(), noopPrompter); + expect(result.models?.providers?.kilocode?.models?.map((model) => model.id)).toEqual([ + "anthropic/claude-opus-4.6", + ]); + expect(result.models?.providers?.minimax?.models?.map((model) => model.id)).toEqual([ + "MiniMax-M2.1", + ]); + }); +}); diff --git a/src/commands/configure.gateway-auth.ts b/src/commands/configure.gateway-auth.ts index d39f6ef6246..479f9e7d82d 100644 --- a/src/commands/configure.gateway-auth.ts +++ b/src/commands/configure.gateway-auth.ts @@ -8,6 +8,7 @@ import { applyModelAllowlist, applyModelFallbacksFromSelection, applyPrimaryModel, + pruneKilocodeProviderModelsToAllowlist, promptDefaultModel, promptModelAllowlist, } from "./model-picker.js"; @@ -126,6 +127,7 @@ export async function promptAuthConfig( }); if (allowlistSelection.models) { next = applyModelAllowlist(next, allowlistSelection.models); + next = pruneKilocodeProviderModelsToAllowlist(next, allowlistSelection.models); next = applyModelFallbacksFromSelection(next, allowlistSelection.models); } } diff --git a/src/commands/model-picker.test.ts b/src/commands/model-picker.test.ts index 76ced67ba15..7ea42e5d39f 100644 --- a/src/commands/model-picker.test.ts +++ b/src/commands/model-picker.test.ts @@ -3,6 +3,7 @@ import type { OpenClawConfig } from "../config/config.js"; import { applyModelAllowlist, applyModelFallbacksFromSelection, + pruneKilocodeProviderModelsToAllowlist, promptDefaultModel, promptModelAllowlist, } from "./model-picker.js"; @@ -60,6 +61,18 @@ function createSelectAllMultiselect() { return vi.fn(async (params) => params.options.map((option: { value: string }) => option.value)); } +function makeProviderModel(id: string, name: string) { + return { + id, + name, + reasoning: false, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 200000, + maxTokens: 8192, + }; +} + describe("promptDefaultModel", () => { it("supports configuring vLLM during onboarding", async () => { loadModelCatalog.mockResolvedValue([ @@ -249,3 +262,60 @@ describe("applyModelFallbacksFromSelection", () => { }); }); }); + +describe("pruneKilocodeProviderModelsToAllowlist", () => { + it("keeps only selected model definitions in provider configs", () => { + const config = { + models: { + providers: { + kilocode: { + baseUrl: "https://api.kilo.ai/api/gateway/", + api: "openai-completions", + models: [ + makeProviderModel("anthropic/claude-opus-4.6", "Claude Opus 4.6"), + makeProviderModel("minimax/minimax-m2.5:free", "MiniMax M2.5 (Free)"), + ], + }, + }, + }, + } as OpenClawConfig; + + const next = pruneKilocodeProviderModelsToAllowlist(config, [ + "kilocode/anthropic/claude-opus-4.6", + ]); + + expect(next.models?.providers?.kilocode?.models?.map((model) => model.id)).toEqual([ + "anthropic/claude-opus-4.6", + ]); + }); + + it("does not modify non-kilo provider model catalogs", () => { + const config = { + models: { + providers: { + kilocode: { + baseUrl: "https://api.kilo.ai/api/gateway/", + api: "openai-completions", + models: [makeProviderModel("anthropic/claude-opus-4.6", "Claude Opus 4.6")], + }, + minimax: { + baseUrl: "https://api.minimax.io/anthropic", + api: "anthropic-messages", + models: [makeProviderModel("MiniMax-M2.5", "MiniMax M2.5")], + }, + }, + }, + } as OpenClawConfig; + + const next = pruneKilocodeProviderModelsToAllowlist(config, [ + "kilocode/anthropic/claude-opus-4.6", + ]); + + expect(next.models?.providers?.kilocode?.models?.map((model) => model.id)).toEqual([ + "anthropic/claude-opus-4.6", + ]); + expect(next.models?.providers?.minimax?.models?.map((model) => model.id)).toEqual([ + "MiniMax-M2.5", + ]); + }); +}); diff --git a/src/commands/model-picker.ts b/src/commands/model-picker.ts index db794210354..c843d637241 100644 --- a/src/commands/model-picker.ts +++ b/src/commands/model-picker.ts @@ -102,6 +102,34 @@ function normalizeModelKeys(values: string[]): string[] { return next; } +function splitModelKey(value: string): { provider: string; modelId: string } | null { + const key = String(value ?? "").trim(); + const slashIndex = key.indexOf("/"); + if (slashIndex <= 0 || slashIndex >= key.length - 1) { + return null; + } + const provider = normalizeProviderId(key.slice(0, slashIndex)); + const modelId = key.slice(slashIndex + 1).trim(); + if (!provider || !modelId) { + return null; + } + return { provider, modelId }; +} + +function selectedModelIdsByProvider(modelKeys: string[]): Map> { + const out = new Map>(); + for (const key of modelKeys) { + const split = splitModelKey(key); + if (!split) { + continue; + } + const existing = out.get(split.provider) ?? new Set(); + existing.add(split.modelId.toLowerCase()); + out.set(split.provider, existing); + } + return out; +} + function addModelSelectOption(params: { entry: { provider: string; @@ -521,6 +549,66 @@ export function applyModelAllowlist(cfg: OpenClawConfig, models: string[]): Open }; } +export function pruneKilocodeProviderModelsToAllowlist( + cfg: OpenClawConfig, + selectedModels: string[], +): OpenClawConfig { + const normalized = normalizeModelKeys(selectedModels); + if (normalized.length === 0) { + return cfg; + } + const providers = cfg.models?.providers; + if (!providers) { + return cfg; + } + + const selectedByProvider = selectedModelIdsByProvider(normalized); + // Keep this scoped to Kilo Gateway: do not mutate other providers here. + const selectedKilocodeIds = selectedByProvider.get("kilocode"); + if (!selectedKilocodeIds || selectedKilocodeIds.size === 0) { + return cfg; + } + let mutated = false; + const nextProviders: NonNullable["providers"] = { ...providers }; + + for (const [providerIdRaw, providerConfig] of Object.entries(providers)) { + if (!providerConfig || !Array.isArray(providerConfig.models)) { + continue; + } + const providerId = normalizeProviderId(providerIdRaw); + if (providerId !== "kilocode") { + continue; + } + const filteredModels = providerConfig.models.filter((model) => + selectedKilocodeIds.has( + String(model.id ?? "") + .trim() + .toLowerCase(), + ), + ); + if (filteredModels.length === providerConfig.models.length) { + continue; + } + mutated = true; + nextProviders[providerIdRaw] = { + ...providerConfig, + models: filteredModels, + }; + } + + if (!mutated) { + return cfg; + } + + return { + ...cfg, + models: { + mode: cfg.models?.mode ?? "merge", + providers: nextProviders, + }, + }; +} + export function applyModelFallbacksFromSelection( cfg: OpenClawConfig, selection: string[], diff --git a/src/commands/onboard-auth.config-core.kilocode.test.ts b/src/commands/onboard-auth.config-core.kilocode.test.ts index 33e16c6c88a..38dc802492f 100644 --- a/src/commands/onboard-auth.config-core.kilocode.test.ts +++ b/src/commands/onboard-auth.config-core.kilocode.test.ts @@ -21,6 +21,17 @@ import { } from "./onboard-auth.models.js"; const emptyCfg: OpenClawConfig = {}; +const KILOCODE_MODEL_IDS = [ + "anthropic/claude-opus-4.6", + "z-ai/glm-5:free", + "minimax/minimax-m2.5:free", + "anthropic/claude-sonnet-4.5", + "openai/gpt-5.2", + "google/gemini-3-pro-preview", + "google/gemini-3-flash-preview", + "x-ai/grok-code-fast-1", + "moonshotai/kimi-k2.5", +]; describe("Kilo Gateway provider config", () => { describe("constants", () => { @@ -68,6 +79,33 @@ describe("Kilo Gateway provider config", () => { expect(modelIds).toContain(KILOCODE_DEFAULT_MODEL_ID); }); + it("surfaces the full Kilo model catalog", () => { + const result = applyKilocodeProviderConfig(emptyCfg); + const provider = result.models?.providers?.kilocode; + const modelIds = provider?.models?.map((m) => m.id) ?? []; + for (const modelId of KILOCODE_MODEL_IDS) { + expect(modelIds).toContain(modelId); + } + }); + + it("appends missing catalog models to existing Kilo provider config", () => { + const result = applyKilocodeProviderConfig({ + models: { + providers: { + kilocode: { + baseUrl: KILOCODE_BASE_URL, + api: "openai-completions", + models: [buildKilocodeModelDefinition()], + }, + }, + }, + }); + const modelIds = result.models?.providers?.kilocode?.models?.map((m) => m.id) ?? []; + for (const modelId of KILOCODE_MODEL_IDS) { + expect(modelIds).toContain(modelId); + } + }); + it("sets Kilo Gateway alias in agent default models", () => { const result = applyKilocodeProviderConfig(emptyCfg); const agentModel = result.agents?.defaults?.models?.[KILOCODE_DEFAULT_MODEL_REF]; diff --git a/src/commands/onboard-auth.config-core.ts b/src/commands/onboard-auth.config-core.ts index f8b45a68017..f5722f94bd7 100644 --- a/src/commands/onboard-auth.config-core.ts +++ b/src/commands/onboard-auth.config-core.ts @@ -4,6 +4,7 @@ import { HUGGINGFACE_MODEL_CATALOG, } from "../agents/huggingface-models.js"; import { + buildKilocodeProvider, buildKimiCodingProvider, buildQianfanProvider, buildXiaomiProvider, @@ -60,12 +61,10 @@ import { applyProviderConfigWithModelCatalog, } from "./onboard-auth.config-shared.js"; import { - buildKilocodeModelDefinition, buildMistralModelDefinition, buildZaiModelDefinition, buildMoonshotModelDefinition, buildXaiModelDefinition, - KILOCODE_DEFAULT_MODEL_ID, MISTRAL_BASE_URL, MISTRAL_DEFAULT_MODEL_ID, QIANFAN_BASE_URL, @@ -447,15 +446,14 @@ export function applyKilocodeProviderConfig(cfg: OpenClawConfig): OpenClawConfig alias: models[KILOCODE_DEFAULT_MODEL_REF]?.alias ?? "Kilo Gateway", }; - const defaultModel = buildKilocodeModelDefinition(); + const kilocodeModels = buildKilocodeProvider().models ?? []; - return applyProviderConfigWithDefaultModel(cfg, { + return applyProviderConfigWithModelCatalog(cfg, { agentModels: models, providerId: "kilocode", api: "openai-completions", baseUrl: KILOCODE_BASE_URL, - defaultModel, - defaultModelId: KILOCODE_DEFAULT_MODEL_ID, + catalogModels: kilocodeModels, }); } diff --git a/src/providers/kilocode-shared.ts b/src/providers/kilocode-shared.ts index ef90edd1b78..760488fe01e 100644 --- a/src/providers/kilocode-shared.ts +++ b/src/providers/kilocode-shared.ts @@ -2,8 +2,90 @@ 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 type KilocodeModelCatalogEntry = { + id: string; + name: string; + reasoning: boolean; + input: Array<"text" | "image">; + contextWindow?: number; + maxTokens?: number; +}; +export const KILOCODE_MODEL_CATALOG: KilocodeModelCatalogEntry[] = [ + { + id: KILOCODE_DEFAULT_MODEL_ID, + name: KILOCODE_DEFAULT_MODEL_NAME, + reasoning: true, + input: ["text", "image"], + contextWindow: 1000000, + maxTokens: 128000, + }, + { + id: "z-ai/glm-5:free", + name: "GLM-5 (Free)", + reasoning: true, + input: ["text"], + contextWindow: 202800, + maxTokens: 131072, + }, + { + id: "minimax/minimax-m2.5:free", + name: "MiniMax M2.5 (Free)", + reasoning: true, + input: ["text"], + contextWindow: 204800, + maxTokens: 131072, + }, + { + id: "anthropic/claude-sonnet-4.5", + name: "Claude Sonnet 4.5", + reasoning: true, + input: ["text", "image"], + contextWindow: 1000000, + maxTokens: 64000, + }, + { + id: "openai/gpt-5.2", + name: "GPT-5.2", + reasoning: true, + input: ["text", "image"], + contextWindow: 400000, + maxTokens: 128000, + }, + { + id: "google/gemini-3-pro-preview", + name: "Gemini 3 Pro Preview", + reasoning: true, + input: ["text", "image"], + contextWindow: 1048576, + maxTokens: 65536, + }, + { + id: "google/gemini-3-flash-preview", + name: "Gemini 3 Flash Preview", + reasoning: true, + input: ["text", "image"], + contextWindow: 1048576, + maxTokens: 65535, + }, + { + id: "x-ai/grok-code-fast-1", + name: "Grok Code Fast 1", + reasoning: true, + input: ["text"], + contextWindow: 256000, + maxTokens: 10000, + }, + { + id: "moonshotai/kimi-k2.5", + name: "Kimi K2.5", + reasoning: true, + input: ["text", "image"], + contextWindow: 262144, + maxTokens: 65535, + }, +]; +export const KILOCODE_DEFAULT_CONTEXT_WINDOW = 1000000; +export const KILOCODE_DEFAULT_MAX_TOKENS = 128000; export const KILOCODE_DEFAULT_COST = { input: 0, output: 0,