Skip to content

Commit ad92181

Browse files
authored
feat: add Kilo as a native provider (anomalyco#13765)
1 parent c56f4aa commit ad92181

File tree

2 files changed

+199
-0
lines changed

2 files changed

+199
-0
lines changed

packages/opencode/src/provider/provider.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -578,6 +578,18 @@ export namespace Provider {
578578
},
579579
}
580580
},
581+
kilo: async () => {
582+
return {
583+
autoload: true,
584+
options: {
585+
baseURL: "https://api.kilo.ai/api/gateway",
586+
headers: {
587+
"HTTP-Referer": "https://opencode.ai/",
588+
"X-Title": "opencode",
589+
},
590+
},
591+
}
592+
},
581593
}
582594

583595
export const Model = z

packages/opencode/test/provider/provider.test.ts

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2218,3 +2218,190 @@ test("Google Vertex: supports OpenAI compatible models", async () => {
22182218
},
22192219
})
22202220
})
2221+
2222+
test("kilo provider loaded from config with env var", async () => {
2223+
await using tmp = await tmpdir({
2224+
init: async (dir) => {
2225+
await Bun.write(
2226+
path.join(dir, "opencode.json"),
2227+
JSON.stringify({
2228+
$schema: "https://opencode.ai/config.json",
2229+
provider: {
2230+
kilo: {
2231+
name: "Kilo",
2232+
npm: "@ai-sdk/openai-compatible",
2233+
env: ["KILO_API_KEY"],
2234+
api: "https://api.kilo.ai/api/gateway",
2235+
models: {
2236+
"anthropic/claude-sonnet-4-20250514": {
2237+
name: "Claude Sonnet 4 (via Kilo)",
2238+
tool_call: true,
2239+
attachment: true,
2240+
temperature: true,
2241+
limit: { context: 200000, output: 16384 },
2242+
},
2243+
},
2244+
},
2245+
},
2246+
}),
2247+
)
2248+
},
2249+
})
2250+
await Instance.provide({
2251+
directory: tmp.path,
2252+
init: async () => {
2253+
Env.set("KILO_API_KEY", "test-kilo-key")
2254+
},
2255+
fn: async () => {
2256+
const providers = await Provider.list()
2257+
expect(providers["kilo"]).toBeDefined()
2258+
expect(providers["kilo"].source).toBe("config")
2259+
expect(providers["kilo"].options.baseURL).toBe(
2260+
"https://api.kilo.ai/api/gateway",
2261+
)
2262+
expect(providers["kilo"].options.headers).toBeDefined()
2263+
expect(providers["kilo"].options.headers["HTTP-Referer"]).toBe(
2264+
"https://opencode.ai/",
2265+
)
2266+
expect(providers["kilo"].options.headers["X-Title"]).toBe("opencode")
2267+
const model =
2268+
providers["kilo"].models["anthropic/claude-sonnet-4-20250514"]
2269+
expect(model).toBeDefined()
2270+
expect(model.name).toBe("Claude Sonnet 4 (via Kilo)")
2271+
},
2272+
})
2273+
})
2274+
2275+
test("kilo provider loaded from config without env var still has custom loader options", async () => {
2276+
await using tmp = await tmpdir({
2277+
init: async (dir) => {
2278+
await Bun.write(
2279+
path.join(dir, "opencode.json"),
2280+
JSON.stringify({
2281+
$schema: "https://opencode.ai/config.json",
2282+
provider: {
2283+
kilo: {
2284+
name: "Kilo",
2285+
npm: "@ai-sdk/openai-compatible",
2286+
env: ["KILO_API_KEY"],
2287+
api: "https://api.kilo.ai/api/gateway",
2288+
models: {
2289+
"anthropic/claude-sonnet-4-20250514": {
2290+
name: "Claude Sonnet 4 (via Kilo)",
2291+
tool_call: true,
2292+
attachment: true,
2293+
temperature: true,
2294+
limit: { context: 200000, output: 16384 },
2295+
},
2296+
},
2297+
},
2298+
},
2299+
}),
2300+
)
2301+
},
2302+
})
2303+
await Instance.provide({
2304+
directory: tmp.path,
2305+
fn: async () => {
2306+
const providers = await Provider.list()
2307+
expect(providers["kilo"]).toBeDefined()
2308+
expect(providers["kilo"].source).toBe("config")
2309+
expect(providers["kilo"].options.baseURL).toBe(
2310+
"https://api.kilo.ai/api/gateway",
2311+
)
2312+
expect(providers["kilo"].options.headers["HTTP-Referer"]).toBe(
2313+
"https://opencode.ai/",
2314+
)
2315+
expect(providers["kilo"].options.headers["X-Title"]).toBe("opencode")
2316+
},
2317+
})
2318+
})
2319+
2320+
test("kilo provider config options deeply merged with custom loader", async () => {
2321+
await using tmp = await tmpdir({
2322+
init: async (dir) => {
2323+
await Bun.write(
2324+
path.join(dir, "opencode.json"),
2325+
JSON.stringify({
2326+
$schema: "https://opencode.ai/config.json",
2327+
provider: {
2328+
kilo: {
2329+
name: "Kilo",
2330+
npm: "@ai-sdk/openai-compatible",
2331+
env: ["KILO_API_KEY"],
2332+
api: "https://api.kilo.ai/api/gateway",
2333+
options: {
2334+
apiKey: "custom-key-from-config",
2335+
},
2336+
models: {
2337+
"openai/gpt-4o": {
2338+
name: "GPT-4o (via Kilo)",
2339+
tool_call: true,
2340+
limit: { context: 128000, output: 16384 },
2341+
},
2342+
},
2343+
},
2344+
},
2345+
}),
2346+
)
2347+
},
2348+
})
2349+
await Instance.provide({
2350+
directory: tmp.path,
2351+
init: async () => {
2352+
Env.set("KILO_API_KEY", "test-kilo-key")
2353+
},
2354+
fn: async () => {
2355+
const providers = await Provider.list()
2356+
expect(providers["kilo"]).toBeDefined()
2357+
expect(providers["kilo"].options.headers["HTTP-Referer"]).toBe(
2358+
"https://opencode.ai/",
2359+
)
2360+
expect(providers["kilo"].options.apiKey).toBe("custom-key-from-config")
2361+
expect(providers["kilo"].models["openai/gpt-4o"]).toBeDefined()
2362+
expect(providers["kilo"].models["openai/gpt-4o"].name).toBe(
2363+
"GPT-4o (via Kilo)",
2364+
)
2365+
},
2366+
})
2367+
})
2368+
2369+
test("kilo provider with api key set via config apiKey", async () => {
2370+
await using tmp = await tmpdir({
2371+
init: async (dir) => {
2372+
await Bun.write(
2373+
path.join(dir, "opencode.json"),
2374+
JSON.stringify({
2375+
$schema: "https://opencode.ai/config.json",
2376+
provider: {
2377+
kilo: {
2378+
name: "Kilo",
2379+
npm: "@ai-sdk/openai-compatible",
2380+
env: ["KILO_API_KEY"],
2381+
api: "https://api.kilo.ai/api/gateway",
2382+
options: {
2383+
apiKey: "config-api-key",
2384+
},
2385+
models: {
2386+
"anthropic/claude-sonnet-4-20250514": {
2387+
name: "Claude Sonnet 4 (via Kilo)",
2388+
tool_call: true,
2389+
limit: { context: 200000, output: 16384 },
2390+
},
2391+
},
2392+
},
2393+
},
2394+
}),
2395+
)
2396+
},
2397+
})
2398+
await Instance.provide({
2399+
directory: tmp.path,
2400+
fn: async () => {
2401+
const providers = await Provider.list()
2402+
expect(providers["kilo"]).toBeDefined()
2403+
expect(providers["kilo"].source).toBe("config")
2404+
expect(providers["kilo"].options.apiKey).toBe("config-api-key")
2405+
},
2406+
})
2407+
})

0 commit comments

Comments
 (0)