Skip to content
This repository was archived by the owner on Oct 10, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions apps/server/src/provider/Layers/KiloProvider.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import assert from "node:assert/strict";

import { describe, it } from "vitest";

import { kiloDiscoveredToServerModels } from "./KiloProvider.ts";

/**
* Guards the wire-up of Kilo's previously-dead dynamic model discovery. The
* end-to-end path (live server query) degrades gracefully and can't be
* integration-tested here, but this pins the pure mapper that feeds the
* snapshot: discovered models must become non-custom `ServerProviderModel`s
* with default capabilities so they render in the picker and dedupe against
* custom slugs in providerModelsFromSettings.
*/
describe("KiloProvider model discovery mapping", () => {
it("maps discovered Kilo models to non-custom server models with default capabilities", () => {
const result = kiloDiscoveredToServerModels([
{ slug: "openai/gpt-5", name: "OpenAI / GPT-5", variants: ["high"], connected: true },
{ slug: "anthropic/claude", name: "Anthropic / Claude", connected: false },
]);

assert.equal(result.length, 2);
const [first, second] = result;
if (!first || !second) {
assert.fail("expected two mapped models");
}

assert.equal(first.slug, "openai/gpt-5");
assert.equal(first.name, "OpenAI / GPT-5");
assert.equal(first.isCustom, false);
assert.ok(first.capabilities, "discovered models carry default capabilities");
assert.equal(second.slug, "anthropic/claude");
assert.equal(second.isCustom, false);
});

it("returns an empty list when nothing is discovered", () => {
assert.deepEqual(kiloDiscoveredToServerModels([]), []);
});
});
66 changes: 65 additions & 1 deletion apps/server/src/provider/Layers/KiloProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ import {
ProviderDriverKind,
type GenericProviderSettings,
type ModelCapabilities,
type ServerProviderModel,
} from "@t3tools/contracts";
import * as Data from "effect/Data";
import * as Effect from "effect/Effect";
import * as Option from "effect/Option";
import * as Path from "effect/Path";
Expand All @@ -38,6 +40,7 @@ import {
spawnAndCollect,
type ServerProviderDraft,
} from "../providerSnapshot.ts";
import { fetchKiloModels, type KiloDiscoveredModel } from "../../kiloServerManager.ts";

const PROVIDER = ProviderDriverKind.make("kilo");
const KILO_PRESENTATION = {
Expand All @@ -51,6 +54,57 @@ const DEFAULT_KILO_MODEL_CAPABILITIES: ModelCapabilities = createModelCapabiliti

export type KiloSettings = GenericProviderSettings;

class KiloModelDiscoveryError extends Data.TaggedError("KiloModelDiscoveryError")<{
readonly message: string;
}> {}

/**
* Map Kilo's discovered models into the contract `ServerProviderModel` shape
* the snapshot consumes. Mirrors OpenCode's `flattenOpenCodeModels`: discovered
* models are `isCustom: false` and carry the default capabilities so the trait
* picker still renders. The `connected` flag is informational — Kilo lists all
* configured providers, so we surface every discovered model (the user can pick
* one before its upstream provider connects).
*/
export function kiloDiscoveredToServerModels(
discovered: ReadonlyArray<KiloDiscoveredModel>,
): ReadonlyArray<ServerProviderModel> {
return discovered.map((model) => ({
slug: model.slug,
name: model.name,
isCustom: false,
capabilities: DEFAULT_KILO_MODEL_CAPABILITIES,
}));
}

/**
* Best-effort dynamic model discovery. `fetchKiloModels` spins up and tears
* down its own throwaway `KiloServerManager` to query the live Kilo server.
* Any failure — Kilo not installed, server unreachable, SDK error, or timeout —
* degrades to an empty list so the snapshot still publishes with just the
* custom models (the pre-wire-up behavior). Bounded by `DEFAULT_TIMEOUT_MS`.
*/
const discoverKiloModels = (
kiloSettings: KiloSettings,
): Effect.Effect<ReadonlyArray<KiloDiscoveredModel>> =>
Effect.tryPromise({
try: () => {
const binaryPath = kiloSettings.binaryPath.trim();
return fetchKiloModels(binaryPath ? { binaryPath } : {});
},
catch: (cause) =>
new KiloModelDiscoveryError({
message: cause instanceof Error ? cause.message : String(cause),
}),
}).pipe(
Effect.timeoutOption(DEFAULT_TIMEOUT_MS),
Effect.result,
Effect.map(
(result): ReadonlyArray<KiloDiscoveredModel> =>
Result.isSuccess(result) && Option.isSome(result.success) ? result.success.value : [],
),
);

const runKiloCommand = Effect.fn("runKiloCommand")(function* (
kiloSettings: KiloSettings,
args: ReadonlyArray<string>,
Expand Down Expand Up @@ -157,11 +211,21 @@ export const checkKiloProviderStatus = Effect.fn("checkKiloProviderStatus")(func
});
}

// The CLI is healthy — query the live Kilo server for its configured models
// and merge them (deduped by slug) with any user-supplied custom slugs.
const discovered = yield* discoverKiloModels(kiloSettings);
const readyModels = providerModelsFromSettings(
kiloDiscoveredToServerModels(discovered),
PROVIDER,
kiloSettings.customModels,
DEFAULT_KILO_MODEL_CAPABILITIES,
);

return buildServerProvider({
presentation: KILO_PRESENTATION,
enabled: true,
checkedAt,
models: allModels,
models: readyModels,
probe: {
installed: true,
version: parsedVersion,
Expand Down
Loading