diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index cdbad6637848..ecb932001bd1 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -578,6 +578,18 @@ export namespace Provider { }, } }, + kilo: async () => { + return { + autoload: true, + options: { + baseURL: "https://api.kilo.ai/api/gateway", + headers: { + "HTTP-Referer": "https://opencode.ai/", + "X-Title": "opencode", + }, + }, + } + }, } export const Model = z diff --git a/packages/opencode/test/provider/provider.test.ts b/packages/opencode/test/provider/provider.test.ts index 0a5aa415131c..c7f8a5edef37 100644 --- a/packages/opencode/test/provider/provider.test.ts +++ b/packages/opencode/test/provider/provider.test.ts @@ -2218,3 +2218,190 @@ test("Google Vertex: supports OpenAI compatible models", async () => { }, }) }) + +test("kilo provider loaded from config with env var", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + provider: { + kilo: { + name: "Kilo", + npm: "@ai-sdk/openai-compatible", + env: ["KILO_API_KEY"], + api: "https://api.kilo.ai/api/gateway", + models: { + "anthropic/claude-sonnet-4-20250514": { + name: "Claude Sonnet 4 (via Kilo)", + tool_call: true, + attachment: true, + temperature: true, + limit: { context: 200000, output: 16384 }, + }, + }, + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + init: async () => { + Env.set("KILO_API_KEY", "test-kilo-key") + }, + fn: async () => { + const providers = await Provider.list() + expect(providers["kilo"]).toBeDefined() + expect(providers["kilo"].source).toBe("config") + expect(providers["kilo"].options.baseURL).toBe( + "https://api.kilo.ai/api/gateway", + ) + expect(providers["kilo"].options.headers).toBeDefined() + expect(providers["kilo"].options.headers["HTTP-Referer"]).toBe( + "https://opencode.ai/", + ) + expect(providers["kilo"].options.headers["X-Title"]).toBe("opencode") + const model = + providers["kilo"].models["anthropic/claude-sonnet-4-20250514"] + expect(model).toBeDefined() + expect(model.name).toBe("Claude Sonnet 4 (via Kilo)") + }, + }) +}) + +test("kilo provider loaded from config without env var still has custom loader options", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + provider: { + kilo: { + name: "Kilo", + npm: "@ai-sdk/openai-compatible", + env: ["KILO_API_KEY"], + api: "https://api.kilo.ai/api/gateway", + models: { + "anthropic/claude-sonnet-4-20250514": { + name: "Claude Sonnet 4 (via Kilo)", + tool_call: true, + attachment: true, + temperature: true, + limit: { context: 200000, output: 16384 }, + }, + }, + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const providers = await Provider.list() + expect(providers["kilo"]).toBeDefined() + expect(providers["kilo"].source).toBe("config") + expect(providers["kilo"].options.baseURL).toBe( + "https://api.kilo.ai/api/gateway", + ) + expect(providers["kilo"].options.headers["HTTP-Referer"]).toBe( + "https://opencode.ai/", + ) + expect(providers["kilo"].options.headers["X-Title"]).toBe("opencode") + }, + }) +}) + +test("kilo provider config options deeply merged with custom loader", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + provider: { + kilo: { + name: "Kilo", + npm: "@ai-sdk/openai-compatible", + env: ["KILO_API_KEY"], + api: "https://api.kilo.ai/api/gateway", + options: { + apiKey: "custom-key-from-config", + }, + models: { + "openai/gpt-4o": { + name: "GPT-4o (via Kilo)", + tool_call: true, + limit: { context: 128000, output: 16384 }, + }, + }, + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + init: async () => { + Env.set("KILO_API_KEY", "test-kilo-key") + }, + fn: async () => { + const providers = await Provider.list() + expect(providers["kilo"]).toBeDefined() + expect(providers["kilo"].options.headers["HTTP-Referer"]).toBe( + "https://opencode.ai/", + ) + expect(providers["kilo"].options.apiKey).toBe("custom-key-from-config") + expect(providers["kilo"].models["openai/gpt-4o"]).toBeDefined() + expect(providers["kilo"].models["openai/gpt-4o"].name).toBe( + "GPT-4o (via Kilo)", + ) + }, + }) +}) + +test("kilo provider with api key set via config apiKey", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + provider: { + kilo: { + name: "Kilo", + npm: "@ai-sdk/openai-compatible", + env: ["KILO_API_KEY"], + api: "https://api.kilo.ai/api/gateway", + options: { + apiKey: "config-api-key", + }, + models: { + "anthropic/claude-sonnet-4-20250514": { + name: "Claude Sonnet 4 (via Kilo)", + tool_call: true, + limit: { context: 200000, output: 16384 }, + }, + }, + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const providers = await Provider.list() + expect(providers["kilo"]).toBeDefined() + expect(providers["kilo"].source).toBe("config") + expect(providers["kilo"].options.apiKey).toBe("config-api-key") + }, + }) +})