Skip to content

Commit 63591a5

Browse files
author
Joshua Chittick
committed
feat: add Kilo model selection
Fetch Kilo model lists, store them in config, and allow selecting models in Slack/UI with runtime support.
1 parent 65f935f commit 63591a5

File tree

9 files changed

+153
-13
lines changed

9 files changed

+153
-13
lines changed

packages/config/dashboard-config.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export type DashboardConfig = {
2929
};
3030
kilo: {
3131
enabled: boolean;
32+
models: string[];
3233
};
3334
qwen: {
3435
enabled: boolean;
@@ -81,7 +82,7 @@ export const defaultDashboardConfig: DashboardConfig = {
8182
codex: { enabled: true, models: [] },
8283
kimi: { enabled: true },
8384
kiro: { enabled: true },
84-
kilo: { enabled: true },
85+
kilo: { enabled: true, models: [] },
8586
qwen: { enabled: true },
8687
},
8788
workspaces: [],
@@ -271,6 +272,7 @@ export const sanitizeDashboardConfig = (config: unknown): DashboardConfig => {
271272
},
272273
kilo: {
273274
enabled: kiloRecord.enabled !== false,
275+
models: Array.from(new Set(asStringArray(kiloRecord.models).filter(Boolean))),
274276
},
275277
qwen: {
276278
enabled: qwenRecord.enabled !== false,

packages/config/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ export {
1212
setOpenCodeModels,
1313
getCodexModels,
1414
setCodexModels,
15+
getKiloModels,
16+
setKiloModels,
1517
DEFAULT_CODEX_MODEL,
1618
getUpdateConfig,
1719
getChannelDetails,

packages/config/local/ode.ts

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@ const agentsSchema = z.object({
5959
}).optional().default({ enabled: true }),
6060
kilo: z.object({
6161
enabled: z.boolean().optional().default(true),
62-
}).optional().default({ enabled: true }),
62+
models: z.array(z.string()).optional().default([]),
63+
}).optional().default({ enabled: true, models: [] }),
6364
qwen: z.object({
6465
enabled: z.boolean().optional().default(true),
6566
}).optional().default({ enabled: true }),
@@ -69,7 +70,7 @@ const agentsSchema = z.object({
6970
codex: { enabled: true, models: [] },
7071
kimi: { enabled: true },
7172
kiro: { enabled: true },
72-
kilo: { enabled: true },
73+
kilo: { enabled: true, models: [] },
7374
qwen: { enabled: true },
7475
});
7576

@@ -156,7 +157,7 @@ const EMPTY_TEMPLATE: OdeConfig = {
156157
codex: { enabled: true, models: [] },
157158
kimi: { enabled: true },
158159
kiro: { enabled: true },
159-
kilo: { enabled: true },
160+
kilo: { enabled: true, models: [] },
160161
qwen: { enabled: true },
161162
},
162163
completeOnboarding: false,
@@ -209,6 +210,9 @@ function normalizeConfig(config: OdeConfig): OdeConfig {
209210
const codexModels = Array.from(new Set((config.agents?.codex?.models ?? [])
210211
.map((model) => model.trim())
211212
.filter(Boolean)));
213+
const kiloModels = Array.from(new Set((config.agents?.kilo?.models ?? [])
214+
.map((model) => model.trim())
215+
.filter(Boolean)));
212216
const completeOnboarding = config.completeOnboarding === true;
213217
const workspaces = config.workspaces.map((workspace) => ({
214218
...workspace,
@@ -248,6 +252,7 @@ function normalizeConfig(config: OdeConfig): OdeConfig {
248252
},
249253
kilo: {
250254
enabled: config.agents?.kilo?.enabled ?? true,
255+
models: kiloModels,
251256
},
252257
qwen: {
253258
enabled: config.agents?.qwen?.enabled ?? true,
@@ -400,6 +405,24 @@ export function setCodexModels(models: string[]): void {
400405
});
401406
}
402407

408+
export function getKiloModels(): string[] {
409+
return getAgentsConfig().kilo.models ?? [];
410+
}
411+
412+
export function setKiloModels(models: string[]): void {
413+
const config = loadOdeConfig();
414+
saveOdeConfig({
415+
...config,
416+
agents: {
417+
...config.agents,
418+
kilo: {
419+
...config.agents.kilo,
420+
models,
421+
},
422+
},
423+
});
424+
}
425+
403426
export function getUpdateConfig(): UpdateConfig {
404427
return loadOdeConfig().updates;
405428
}

packages/core/runtime.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,16 @@ type RuntimeState = {
3535
stateMachines: Map<string, CoreStateMachine>;
3636
};
3737

38+
function toKiloModel(modelValue: string | null | undefined): OpenCodeOptions["model"] | undefined {
39+
const trimmed = modelValue?.trim();
40+
if (!trimmed) return undefined;
41+
const [providerID = "kilo", ...rest] = trimmed.split("/");
42+
if (rest.length === 0) {
43+
return { providerID: "kilo", modelID: trimmed };
44+
}
45+
return { providerID, modelID: rest.join("/") };
46+
}
47+
3848
function createRuntimeState(): RuntimeState {
3949
return {
4050
liveEventHistory: new Map(),
@@ -116,10 +126,12 @@ export function createCoreRuntime(deps: RuntimeDeps) {
116126
const codexModel = providerId === "codex"
117127
? (channelModel && channelModel.length > 0 ? channelModel : DEFAULT_CODEX_MODEL)
118128
: undefined;
119-
const options: OpenCodeOptions | undefined = agent || codexModel
129+
const kiloModel = providerId === "kilo" ? toKiloModel(channelModel) : undefined;
130+
const options: OpenCodeOptions | undefined = agent || codexModel || kiloModel
120131
? {
121132
...(agent ? { agent } : {}),
122133
...(codexModel ? { model: { providerID: "openai", modelID: codexModel } } : {}),
134+
...(kiloModel ? { model: kiloModel } : {}),
123135
}
124136
: undefined;
125137

packages/core/runtime/selection-reply.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,16 @@ type SelectionDeps = {
2020
agent: AgentAdapter;
2121
};
2222

23+
function toKiloModel(modelValue: string | null | undefined): OpenCodeOptions["model"] | undefined {
24+
const trimmed = modelValue?.trim();
25+
if (!trimmed) return undefined;
26+
const [providerID = "kilo", ...rest] = trimmed.split("/");
27+
if (rest.length === 0) {
28+
return { providerID: "kilo", modelID: trimmed };
29+
}
30+
return { providerID, modelID: rest.join("/") };
31+
}
32+
2333
type HandleSelectionReplyParams = {
2434
deps: SelectionDeps;
2535
state: {
@@ -92,10 +102,12 @@ export async function handleSelectionReply(params: HandleSelectionReplyParams):
92102
const codexModel = providerId === "codex"
93103
? (channelModel && channelModel.length > 0 ? channelModel : DEFAULT_CODEX_MODEL)
94104
: undefined;
95-
const options: OpenCodeOptions | undefined = agent || codexModel
105+
const kiloModel = providerId === "kilo" ? toKiloModel(channelModel) : undefined;
106+
const options: OpenCodeOptions | undefined = agent || codexModel || kiloModel
96107
? {
97108
...(agent ? { agent } : {}),
98109
...(codexModel ? { model: { providerID: "openai", modelID: codexModel } } : {}),
110+
...(kiloModel ? { model: kiloModel } : {}),
99111
}
100112
: undefined;
101113

packages/core/web/server.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ type JsonResponse = {
4141
codex: boolean;
4242
kimi: boolean;
4343
kiro: boolean;
44+
kilo: boolean;
4445
qwen: boolean;
4546
};
4647
providers?: unknown;
@@ -122,6 +123,30 @@ function extractOpenCodeModels(payload: unknown): string[] {
122123
return Array.from(models).sort();
123124
}
124125

126+
async function fetchKiloModels(): Promise<string[]> {
127+
const child = Bun.spawn({
128+
cmd: ["kilo", "models"],
129+
cwd: process.cwd(),
130+
stdout: "pipe",
131+
stderr: "pipe",
132+
env: process.env,
133+
});
134+
const [stdout, stderr, exitCode] = await Promise.all([
135+
new Response(child.stdout).text(),
136+
new Response(child.stderr).text(),
137+
child.exited,
138+
]);
139+
if (exitCode !== 0) {
140+
const details = stderr.trim() || stdout.trim() || "Unknown error";
141+
throw new Error(details);
142+
}
143+
return stdout
144+
.split("\n")
145+
.map((line) => line.trim())
146+
.filter(Boolean)
147+
.sort();
148+
}
149+
125150
function parsePositiveInt(value: string | null, fallback: number, max?: number): number {
126151
if (!value) return fallback;
127152
const parsed = Number(value);
@@ -434,6 +459,9 @@ async function handleRequest(request: Request): Promise<Response> {
434459
const opencodeAvailable = Boolean(Bun.which("opencode"));
435460
let opencodeModels: string[] = [];
436461
let opencodeModelError: string | undefined;
462+
const kiloAvailable = Boolean(Bun.which("kilo"));
463+
let kiloModels: string[] = [];
464+
let kiloModelError: string | undefined;
437465

438466
if (opencodeAvailable) {
439467
try {
@@ -454,6 +482,17 @@ async function handleRequest(request: Request): Promise<Response> {
454482
}
455483
}
456484

485+
if (kiloAvailable) {
486+
try {
487+
kiloModels = await fetchKiloModels();
488+
} catch (error) {
489+
kiloModelError = error instanceof Error ? error.message : String(error);
490+
log.warn("Failed to query Kilo models during agent check", {
491+
error: kiloModelError,
492+
});
493+
}
494+
}
495+
457496
return jsonResponse(200, {
458497
ok: true,
459498
result: {
@@ -462,10 +501,12 @@ async function handleRequest(request: Request): Promise<Response> {
462501
codex: Boolean(Bun.which("codex")),
463502
kimi: Boolean(Bun.which("kimi")),
464503
kiro: Boolean(Bun.which("kiro-cli") || Bun.which("kiro")),
465-
kilo: Boolean(Bun.which("kilo")),
504+
kilo: kiloAvailable,
466505
qwen: Boolean(Bun.which("qwen") || Bun.which("qwen-code")),
467506
opencodeModels,
468507
opencodeModelError,
508+
kiloModels,
509+
kiloModelError,
469510
},
470511
});
471512
}

packages/ims/slack/client.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
getChannelAgentProvider,
1010
getChannelModel,
1111
getOpenCodeModels,
12+
getKiloModels,
1213
isAgentEnabled,
1314
getGitHubInfoForUser,
1415
getChannelSystemMessage,
@@ -222,6 +223,14 @@ function describeSettingsIssues(channelId: string): string[] {
222223
} else if (!modelSet.has(normalizeModel(model))) {
223224
issues.push("Model not available in configured OpenCode models.");
224225
}
226+
} else if (provider === "kilo") {
227+
const models = getKiloModels();
228+
const modelSet = new Set(models.map(normalizeModel));
229+
if (!model) {
230+
issues.push("Model not configured.");
231+
} else if (!modelSet.has(normalizeModel(model))) {
232+
issues.push("Model not available in configured Kilo models.");
233+
}
225234
}
226235

227236
if (!workingDirectory) {

packages/ims/slack/commands.ts

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
getEnabledAgentProviders,
88
getOpenCodeModels,
99
getCodexModels,
10+
getKiloModels,
1011
isAgentEnabled,
1112
getGitHubInfoForUser,
1213
setChannelAgentProvider,
@@ -112,6 +113,7 @@ function buildSettingsModal(params: {
112113
enabledProviders: AgentProvider[];
113114
opencodeModels: string[];
114115
codexModels: string[];
116+
kiloModels: string[];
115117
selectedProvider?: AgentProvider;
116118
selectedModel?: string | null;
117119
workingDirectory?: string | null;
@@ -123,6 +125,7 @@ function buildSettingsModal(params: {
123125
enabledProviders,
124126
opencodeModels,
125127
codexModels,
128+
kiloModels,
126129
selectedProvider = "opencode",
127130
selectedModel,
128131
workingDirectory,
@@ -137,7 +140,9 @@ function buildSettingsModal(params: {
137140
? opencodeModels
138141
: selectedProvider === "codex"
139142
? codexModels
140-
: null;
143+
: selectedProvider === "kilo"
144+
? kiloModels
145+
: null;
141146
const modelOptions = providerModels && selectedProvider === "opencode"
142147
? (opencodeModels.length > 0
143148
? opencodeModels.map((model) => ({
@@ -153,6 +158,13 @@ function buildSettingsModal(params: {
153158
value: model,
154159
})),
155160
]
161+
: providerModels && selectedProvider === "kilo"
162+
? (kiloModels.length > 0
163+
? kiloModels.map((model) => ({
164+
text: { type: "plain_text" as const, text: model },
165+
value: model,
166+
}))
167+
: [{ text: { type: "plain_text" as const, text: "No models configured" }, value: "__none__" }])
156168
: [];
157169

158170
const availableModels = selectedProvider === "codex"
@@ -161,11 +173,17 @@ function buildSettingsModal(params: {
161173
const matchedSelectedModel = findMatchingModel(availableModels, selectedModel);
162174
const initialModel = matchedSelectedModel
163175
? matchedSelectedModel
164-
: (selectedProvider === "codex" ? "__default__" : (opencodeModels[0] ?? "__none__"));
176+
: (selectedProvider === "codex"
177+
? "__default__"
178+
: selectedProvider === "kilo"
179+
? (kiloModels[0] ?? "__none__")
180+
: (opencodeModels[0] ?? "__none__"));
165181
const introText = selectedProvider === "opencode"
166182
? "Configure agent, model (OpenCode), working directory, and base branch for this channel."
167183
: selectedProvider === "codex"
168184
? "Configure agent, optional Codex model, working directory, and base branch for this channel."
185+
: selectedProvider === "kilo"
186+
? "Configure agent, model (Kilo), working directory, and base branch for this channel."
169187
: "Configure agent, working directory, and base branch for this channel.";
170188

171189
const blocks: any[] = [
@@ -420,6 +438,7 @@ export function setupInteractiveHandlers(): void {
420438
enabledProviders,
421439
opencodeModels: getOpenCodeModels(),
422440
codexModels: getCodexModels(),
441+
kiloModels: getKiloModels(),
423442
selectedProvider: toSelectableProvider(getChannelAgentProvider(channelId)),
424443
selectedModel: getChannelModel(channelId),
425444
workingDirectory: resolveChannelCwd(channelId).workingDirectory,
@@ -547,6 +566,7 @@ export function setupInteractiveHandlers(): void {
547566
enabledProviders: getSelectableProviders(),
548567
opencodeModels: getOpenCodeModels(),
549568
codexModels: getCodexModels(),
569+
kiloModels: getKiloModels(),
550570
selectedProvider,
551571
selectedModel,
552572
workingDirectory,
@@ -591,6 +611,14 @@ export function setupInteractiveHandlers(): void {
591611
if (selectedModel && selectedModel !== "__default__" && !findMatchingModel(models, selectedModel)) {
592612
errors[MODEL_BLOCK] = "Model not available in local Codex model list.";
593613
}
614+
} else if (selectedProvider === "kilo") {
615+
if (!selectedModel || selectedModel === "__none__") {
616+
errors[MODEL_BLOCK] = "Select a model.";
617+
}
618+
const models = getKiloModels();
619+
if (selectedModel && !findMatchingModel(models, selectedModel)) {
620+
errors[MODEL_BLOCK] = "Model not available in local Kilo model list.";
621+
}
594622
}
595623

596624
if (Object.keys(errors).length > 0) {
@@ -614,7 +642,11 @@ export function setupInteractiveHandlers(): void {
614642
setChannelModel(channelId, "");
615643
}
616644
}
617-
if (selectedProvider === "claudecode" || selectedProvider === "kimi" || selectedProvider === "kiro" || selectedProvider === "kilo" || selectedProvider === "qwen") {
645+
if (selectedProvider === "kilo" && selectedModel && selectedModel !== "__none__") {
646+
const normalizedSelectedModel = findMatchingModel(getKiloModels(), selectedModel) ?? selectedModel;
647+
setChannelModel(channelId, normalizedSelectedModel);
648+
}
649+
if (selectedProvider === "claudecode" || selectedProvider === "kimi" || selectedProvider === "kiro" || selectedProvider === "qwen") {
618650
setChannelModel(channelId, "");
619651
}
620652

0 commit comments

Comments
 (0)