diff --git a/src/agents/model-catalog.test.ts b/src/agents/model-catalog.test.ts index 791947ad8fa..4e476386116 100644 --- a/src/agents/model-catalog.test.ts +++ b/src/agents/model-catalog.test.ts @@ -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", + }), + ); + }); }); diff --git a/src/agents/model-catalog.ts b/src/agents/model-catalog.ts index beda4dc5848..dcf18e4528b 100644 --- a/src/agents/model-catalog.ts +++ b/src/agents/model-catalog.ts @@ -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 | 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; }; - })(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) { 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..41d6a567011 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"; @@ -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", + ]); + }); +}); 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,