Config: expand Kilo catalog and persist selected Kilo models (#24921)

Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: f5a7e1a385
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
This commit is contained in:
Gustavo Madeira Santana
2026-02-23 21:17:37 -05:00
committed by GitHub
parent 6c441ea797
commit 5239b55c0a
14 changed files with 668 additions and 21 deletions

View File

@@ -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.

View File

@@ -133,6 +133,7 @@ OpenClaw ships with the piai catalog. These providers require **no**
- Example model: `kilocode/anthropic/claude-opus-4.6`
- CLI: `openclaw onboard --kilocode-api-key <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.

View File

@@ -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/<provider>/<model>` (e.g., `kilocode/anthropic/claude-opus-4.6`).

View File

@@ -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");
});
});

View File

@@ -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) {

View File

@@ -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);
}
});
});

View File

@@ -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,
})),
};
}

View File

@@ -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<typeof import("./model-picker.js")>();
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",
]);
});
});

View File

@@ -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);
}
}

View File

@@ -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",
]);
});
});

View File

@@ -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<string, Set<string>> {
const out = new Map<string, Set<string>>();
for (const key of modelKeys) {
const split = splitModelKey(key);
if (!split) {
continue;
}
const existing = out.get(split.provider) ?? new Set<string>();
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<OpenClawConfig["models"]>["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[],

View File

@@ -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];

View File

@@ -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,
});
}

View File

@@ -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,