mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-23 12:58:11 +00:00
Config: expand Kilo catalog and persist selected Kilo models
This commit is contained in:
@@ -103,4 +103,63 @@ describe("loadModelCatalog", () => {
|
||||
expect(spark?.name).toBe("gpt-5.3-codex-spark");
|
||||
expect(spark?.reasoning).toBe(true);
|
||||
});
|
||||
|
||||
it("includes configured provider models from models.json when registry omits them", async () => {
|
||||
const cfg = {
|
||||
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",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
contextWindow: 200000,
|
||||
maxTokens: 8192,
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
},
|
||||
{
|
||||
id: "openai/gpt-5.2",
|
||||
name: "GPT-5.2",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
contextWindow: 400000,
|
||||
maxTokens: 8192,
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies OpenClawConfig;
|
||||
|
||||
__setModelCatalogImportForTest(
|
||||
async () =>
|
||||
({
|
||||
AuthStorage: class {},
|
||||
ModelRegistry: class {
|
||||
getAll() {
|
||||
return [{ id: "gpt-4.1", name: "GPT-4.1", provider: "openai" }];
|
||||
}
|
||||
},
|
||||
}) as unknown as PiSdkModule,
|
||||
);
|
||||
|
||||
const result = await loadModelCatalog({ config: cfg });
|
||||
expect(result).toContainEqual(
|
||||
expect.objectContaining({
|
||||
provider: "kilocode",
|
||||
id: "anthropic/claude-opus-4.6",
|
||||
}),
|
||||
);
|
||||
expect(result).toContainEqual(
|
||||
expect.objectContaining({
|
||||
provider: "kilocode",
|
||||
id: "openai/gpt-5.2",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import path from "node:path";
|
||||
import { type OpenClawConfig, loadConfig } from "../config/config.js";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import { resolveOpenClawAgentDir } from "./agent-paths.js";
|
||||
@@ -23,6 +24,14 @@ type DiscoveredModel = {
|
||||
input?: Array<"text" | "image">;
|
||||
};
|
||||
|
||||
type ConfigModelEntry = {
|
||||
id?: unknown;
|
||||
name?: unknown;
|
||||
contextWindow?: unknown;
|
||||
reasoning?: unknown;
|
||||
input?: unknown;
|
||||
};
|
||||
|
||||
type PiSdkModule = typeof import("./pi-model-discovery.js");
|
||||
|
||||
let modelCatalogPromise: Promise<ModelCatalogEntry[]> | null = null;
|
||||
@@ -78,6 +87,74 @@ function createAuthStorage(AuthStorageLike: unknown, path: string) {
|
||||
return new (AuthStorageLike as { new (path: string): unknown })(path);
|
||||
}
|
||||
|
||||
function toPositiveNumber(value: unknown): number | undefined {
|
||||
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
|
||||
}
|
||||
|
||||
function normalizeInput(modalities: unknown): Array<"text" | "image"> | undefined {
|
||||
if (!Array.isArray(modalities) || modalities.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
const hasImage = modalities.some((value) => String(value ?? "").toLowerCase() === "image");
|
||||
return hasImage ? ["text", "image"] : ["text"];
|
||||
}
|
||||
|
||||
function readConfiguredModelsFromConfig(cfg: OpenClawConfig): ModelCatalogEntry[] {
|
||||
const providers = cfg.models?.providers;
|
||||
if (!providers) {
|
||||
return [];
|
||||
}
|
||||
const entries: ModelCatalogEntry[] = [];
|
||||
for (const [providerIdRaw, providerValue] of Object.entries(providers)) {
|
||||
const provider = String(providerIdRaw ?? "").trim();
|
||||
if (!provider || !providerValue) {
|
||||
continue;
|
||||
}
|
||||
const providerModels = (providerValue as { models?: unknown }).models;
|
||||
if (!Array.isArray(providerModels)) {
|
||||
continue;
|
||||
}
|
||||
for (const modelValue of providerModels) {
|
||||
if (!modelValue || typeof modelValue !== "object") {
|
||||
continue;
|
||||
}
|
||||
const model = modelValue as ConfigModelEntry;
|
||||
if (typeof model.id !== "string") {
|
||||
continue;
|
||||
}
|
||||
const id = model.id.trim();
|
||||
if (!id) {
|
||||
continue;
|
||||
}
|
||||
const name = typeof model.name === "string" && model.name.trim() ? model.name.trim() : id;
|
||||
const contextWindow = toPositiveNumber(model.contextWindow);
|
||||
const reasoning = typeof model.reasoning === "boolean" ? model.reasoning : undefined;
|
||||
const input = normalizeInput(model.input);
|
||||
entries.push({ id, name, provider, contextWindow, reasoning, input });
|
||||
}
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
function mergeMissingCatalogEntries(
|
||||
discoveredModels: ModelCatalogEntry[],
|
||||
configuredModels: ModelCatalogEntry[],
|
||||
): ModelCatalogEntry[] {
|
||||
const seen = new Set(
|
||||
discoveredModels.map((entry) => `${entry.provider.toLowerCase()}/${entry.id.toLowerCase()}`),
|
||||
);
|
||||
const merged = [...discoveredModels];
|
||||
for (const entry of configuredModels) {
|
||||
const key = `${entry.provider.toLowerCase()}/${entry.id.toLowerCase()}`;
|
||||
if (seen.has(key)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(key);
|
||||
merged.push(entry);
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
export async function loadModelCatalog(params?: {
|
||||
config?: OpenClawConfig;
|
||||
useCache?: boolean;
|
||||
@@ -111,8 +188,7 @@ export async function loadModelCatalog(params?: {
|
||||
// will keep failing until restart).
|
||||
const piSdk = await importPiSdk();
|
||||
const agentDir = resolveOpenClawAgentDir();
|
||||
const { join } = await import("node:path");
|
||||
const authStorage = createAuthStorage(piSdk.AuthStorage, join(agentDir, "auth.json"));
|
||||
const authStorage = createAuthStorage(piSdk.AuthStorage, path.join(agentDir, "auth.json"));
|
||||
const registry = new (piSdk.ModelRegistry as unknown as {
|
||||
new (
|
||||
authStorage: unknown,
|
||||
@@ -122,7 +198,7 @@ export async function loadModelCatalog(params?: {
|
||||
| {
|
||||
getAll: () => Array<DiscoveredModel>;
|
||||
};
|
||||
})(authStorage, join(agentDir, "models.json"));
|
||||
})(authStorage, path.join(agentDir, "models.json"));
|
||||
const entries = Array.isArray(registry) ? registry : registry.getAll();
|
||||
for (const entry of entries) {
|
||||
const id = String(entry?.id ?? "").trim();
|
||||
@@ -142,6 +218,10 @@ export async function loadModelCatalog(params?: {
|
||||
const input = Array.isArray(entry?.input) ? entry.input : undefined;
|
||||
models.push({ id, name, provider, contextWindow, reasoning, input });
|
||||
}
|
||||
const configuredModels = readConfiguredModelsFromConfig(cfg);
|
||||
const mergedModels = mergeMissingCatalogEntries(models, configuredModels);
|
||||
models.length = 0;
|
||||
models.push(...mergedModels);
|
||||
applyOpenAICodexSparkFallback(models);
|
||||
|
||||
if (models.length === 0) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
131
src/commands/configure.gateway-auth.prompt-auth-config.test.ts
Normal file
131
src/commands/configure.gateway-auth.prompt-auth-config.test.ts
Normal 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",
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { OpenClawConfig } from "../config/config.js";
|
||||
import {
|
||||
applyModelAllowlist,
|
||||
applyModelFallbacksFromSelection,
|
||||
pruneKilocodeProviderModelsToAllowlist,
|
||||
promptDefaultModel,
|
||||
promptModelAllowlist,
|
||||
} from "./model-picker.js";
|
||||
@@ -249,3 +250,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: [
|
||||
{ id: "anthropic/claude-opus-4.6", name: "Claude Opus 4.6" },
|
||||
{ id: "minimax/minimax-m2.5:free", name: "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: [{ id: "anthropic/claude-opus-4.6", name: "Claude Opus 4.6" }],
|
||||
},
|
||||
minimax: {
|
||||
baseUrl: "https://api.minimax.io/anthropic",
|
||||
api: "anthropic-messages",
|
||||
models: [{ id: "MiniMax-M2.5", name: "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",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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[],
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user