From baf12c759e228e9389f7d524aac3a3e987a05d0f Mon Sep 17 00:00:00 2001 From: vkeerthivikram Date: Tue, 23 Jun 2026 12:35:08 +0530 Subject: [PATCH 01/92] feat: add AI image generation tool Port the legacy generate_image tool to the opencode-based CLI as a Kilo-owned tool gated by experimental.image_generation config flag. - New generate_image tool with prompt/path/image/model params - Routes through Kilo Gateway (zero-config) or BYO OpenRouter key - Supports text-to-image generation and image editing - Dynamic model discovery via GET /kilo/models/images endpoint - VS Code settings toggle + live model dropdown in Experimental tab - Writes image to disk and returns inline FilePart attachment - Fallback model catalog for offline resilience --- .changeset/image-generation.md | 5 + .../1782130204245-image-generation-tool.md | 154 +++++++++++ packages/kilo-docs/source-links.md | 3 + packages/kilo-gateway/src/api/models.ts | 99 +++++-- packages/kilo-gateway/src/index.ts | 8 +- packages/kilo-gateway/src/server/routes.ts | 88 ++++++ packages/kilo-vscode/src/KiloProvider.ts | 16 ++ .../src/image-generation/models.ts | 41 +++ packages/kilo-vscode/webview-ui/src/App.tsx | 21 +- .../components/settings/ExperimentalTab.tsx | 37 +++ .../webview-ui/src/context/image-models.tsx | 39 +++ .../kilo-vscode/webview-ui/src/i18n/ar.ts | 6 + .../kilo-vscode/webview-ui/src/i18n/br.ts | 6 + .../kilo-vscode/webview-ui/src/i18n/bs.ts | 6 + .../kilo-vscode/webview-ui/src/i18n/da.ts | 6 + .../kilo-vscode/webview-ui/src/i18n/de.ts | 6 + .../kilo-vscode/webview-ui/src/i18n/en.ts | 6 + .../kilo-vscode/webview-ui/src/i18n/es.ts | 6 + .../kilo-vscode/webview-ui/src/i18n/fr.ts | 6 + .../kilo-vscode/webview-ui/src/i18n/it.ts | 6 + .../kilo-vscode/webview-ui/src/i18n/ja.ts | 6 + .../kilo-vscode/webview-ui/src/i18n/ko.ts | 6 + .../kilo-vscode/webview-ui/src/i18n/nl.ts | 6 + .../kilo-vscode/webview-ui/src/i18n/no.ts | 6 + .../kilo-vscode/webview-ui/src/i18n/pl.ts | 6 + .../kilo-vscode/webview-ui/src/i18n/ru.ts | 6 + .../kilo-vscode/webview-ui/src/i18n/th.ts | 6 + .../kilo-vscode/webview-ui/src/i18n/tr.ts | 6 + .../kilo-vscode/webview-ui/src/i18n/uk.ts | 6 + .../kilo-vscode/webview-ui/src/i18n/zh.ts | 6 + .../kilo-vscode/webview-ui/src/i18n/zht.ts | 6 + .../webview-ui/src/types/messages/config.ts | 2 + .../src/types/messages/extension-messages.ts | 6 + .../src/types/messages/webview-messages.ts | 5 + packages/opencode/src/config/config.ts | 4 + .../server/httpapi/groups/kilo-gateway.ts | 18 ++ .../server/httpapi/handlers/kilo-gateway.ts | 23 ++ .../src/kilocode/tool/generate-image.ts | 252 ++++++++++++++++++ .../src/kilocode/tool/generate-image.txt | 7 + .../opencode/src/kilocode/tool/registry.ts | 20 +- packages/opencode/src/tool/registry.ts | 3 + .../kilocode/session-compaction-cap.test.ts | 2 + .../session-prompt-compaction-safety.test.ts | 2 + .../session-prompt-permission-refresh.test.ts | 2 + ...l-registry-indexing-import-failure.test.ts | 1 + .../kilocode/tool-registry-indexing.test.ts | 6 + ...l-registry-semantic-import-failure.test.ts | 1 + .../test/kilocode/tool/generate-image.test.ts | 164 ++++++++++++ packages/opencode/test/session/prompt.test.ts | 2 + .../test/session/snapshot-tool-race.test.ts | 2 + packages/opencode/test/tool/registry.test.ts | 2 + packages/sdk/js/src/v2/gen/sdk.gen.ts | 39 +++ packages/sdk/js/src/v2/gen/types.gen.ts | 34 +++ packages/sdk/openapi.json | 243 +++++++++++++++-- 54 files changed, 1416 insertions(+), 55 deletions(-) create mode 100644 .changeset/image-generation.md create mode 100644 .kilo/plans/1782130204245-image-generation-tool.md create mode 100644 packages/kilo-vscode/src/image-generation/models.ts create mode 100644 packages/kilo-vscode/webview-ui/src/context/image-models.tsx create mode 100644 packages/opencode/src/kilocode/tool/generate-image.ts create mode 100644 packages/opencode/src/kilocode/tool/generate-image.txt create mode 100644 packages/opencode/test/kilocode/tool/generate-image.test.ts diff --git a/.changeset/image-generation.md b/.changeset/image-generation.md new file mode 100644 index 00000000000..4c0c22bd289 --- /dev/null +++ b/.changeset/image-generation.md @@ -0,0 +1,5 @@ +--- +"kilo-code": minor +--- + +Add experimental AI image generation tool. Enable via `experimental.image_generation` in config. Supports text-to-image generation and image editing through the Kilo Gateway or a BYO OpenRouter API key. diff --git a/.kilo/plans/1782130204245-image-generation-tool.md b/.kilo/plans/1782130204245-image-generation-tool.md new file mode 100644 index 00000000000..785ac508af0 --- /dev/null +++ b/.kilo/plans/1782130204245-image-generation-tool.md @@ -0,0 +1,154 @@ +# Image Generation Tool — Port from `kilocode-legacy` + +Port the legacy `generate_image` tool to the opencode-based CLI as a Kilo-owned tool, gated by an experimental flag. The tool lets the LLM generate or edit images via the Kilo Gateway (or BYO OpenRouter key), writing the result to disk and rendering it inline in the chat. + +## Status: IMPLEMENTED + +All core tasks are complete. Build artifacts (CLI binary + VSIX) are produced. Remaining items are CI/compliance and manual verification. + +## Context + +The pre-opencode Kilo (`Kilo-Org/kilocode-legacy`, the Cline/Roo-derived VS Code extension) shipped a `generate_image` native tool with a hardcoded model catalog. It was never ported during the migration to the opencode base. This plan resurrects the feature with a key improvement: **dynamic model discovery from the Kilo Gateway** instead of a static list. + +**Current architecture mapping:** +- Kilo tools live in `packages/opencode/src/kilocode/tool/` (no `kilocode_change` markers) and register via `KiloToolRegistry.extra()`. +- `Tool.define(id, Effect.gen(...))` returns `{ description, parameters, execute }`; `execute` returns `{ title, metadata, output, attachments? }`. +- `attachments` is `MessageV2.FilePart[]` — the native mechanism to surface an image inline. +- Config gating uses `experimental.` in `Config.Info`; Kilo keys need `kilocode_change` markers in shared files. +- The opencode server exposes `/kilo/*` routes via an Effect `HttpApiGroup` in `packages/opencode/src/kilocode/server/httpapi/groups/kilo-gateway.ts` with handlers in `handlers/kilo-gateway.ts`. The VS Code extension fetches these via the local `kilo serve` child process. +- The gateway package (`packages/kilo-gateway`) provides `fetchKiloImageModels()` which calls `https://api.kilo.ai/api/openrouter/models` and filters for `output_modalities.includes("image")`. + +## Decisions + +1. **Provider routing** — Kilo Gateway default (zero-config for logged-in users) + BYO OpenRouter key fallback (`OPENROUTER_API_KEY` env var). +2. **Result surface** — write image to disk at the LLM-chosen path **and** return a `FilePart` attachment for inline rendering. +3. **Editing** — v1 includes the optional `image` input param (read existing file as raw bytes, base64-encode, send multimodal text+image message). +4. **Gating** — `experimental.image_generation` (off by default), all clients. +5. **API mechanism** — chat-completions with `modalities: ["image", "text"]`. +6. **Model catalog** — **dynamic fetch from Kilo Gateway** (not hardcoded). The tool reads `experimental.image_generation_model` from config as the default; the VS Code settings UI fetches the live list via `/kilo/models/images`. A hardcoded `FALLBACK_IMAGE_MODELS` list is kept for offline/error resilience and test stability. Default model: `google/gemini-2.5-flash-image`. +7. **Permission** — `write` permission for the output file; `assertExternalDirectoryEffect` for input image path traversal protection. + +## Completed Tasks + +### 1. Tool implementation (Kilo-owned) +- [x] `packages/opencode/src/kilocode/tool/generate-image.ts` — `Tool.define("generate_image")` with params `{ prompt, path, image?, model? }`. Resolves provider via `Auth.Service`, builds chat-completions fetch with `modalities`, parses data-URL response, writes to disk via `AppFileSystem.writeWithDirs`, returns `FilePart` attachment. +- [x] Input image read via `fs/promises.readFile()` (raw bytes, not text-encoding) + `assertExternalDirectoryEffect` path guard. +- [x] Reads `experimental.image_generation_model` config key for default model. +- [x] `packages/opencode/src/kilocode/tool/generate-image.txt` — tool description. + +### 2. Gateway model API +- [x] `packages/kilo-gateway/src/api/models.ts` — refactored shared `fetchRawKiloModels()`, added `fetchKiloImageModels()` (inverse filter: keeps only `output_modalities.includes("image")`). +- [x] Exported `fetchKiloImageModels`, `KiloImageModel`, `KiloImageModelsResult` from `packages/kilo-gateway/src/index.ts`. + +### 3. Server endpoints (opencode HttpApiGroup) +- [x] `packages/opencode/src/kilocode/server/httpapi/groups/kilo-gateway.ts` — added `GET /kilo/models/images` endpoint + `ImageModel` schema. +- [x] `packages/opencode/src/kilocode/server/httpapi/handlers/kilo-gateway.ts` — added `imageModels` handler calling `fetchKiloImageModels()` with proxy auth. +- [x] `packages/kilo-gateway/src/server/routes.ts` — added `POST /image/generations` Hono route (for standalone gateway usage) + `GET /models/images` route. + +### 4. Registry wiring +- [x] `packages/opencode/src/kilocode/tool/registry.ts` — added `GenerateImageTool` to `infos()`, `build()`, `extra()` (gated by `experimental.image_generation`). +- [x] `packages/opencode/src/tool/registry.ts` (shared, `kilocode_change` markers) — added `Auth.Service` dependency + `Auth.defaultLayer` provision. + +### 5. Config +- [x] `packages/opencode/src/config/config.ts` (shared, `kilocode_change` markers) — added `image_generation` and `image_generation_model` to the `experimental` struct. + +### 6. VS Code extension +- [x] `packages/kilo-vscode/webview-ui/src/types/messages/config.ts` — added `image_generation` + `image_generation_model` to `ExperimentalConfig`. +- [x] `packages/kilo-vscode/webview-ui/src/components/settings/ExperimentalTab.tsx` — Image Generation toggle + conditional model dropdown (fetched live via context). +- [x] `packages/kilo-vscode/webview-ui/src/context/image-models.tsx` — `ImageModelsProvider` context that sends `requestImageModels` and listens for `imageModelsLoaded`. +- [x] `packages/kilo-vscode/webview-ui/src/App.tsx` — registered `ImageModelsProvider` in the provider tree. +- [x] `packages/kilo-vscode/src/image-generation/models.ts` — `fetchImageModels()` helper (calls `/kilo/models/images` via the local connection service). +- [x] `packages/kilo-vscode/src/KiloProvider.ts` — `fetchAndSendImageModels()` handler + `requestImageModels` case + cached message. +- [x] Message types: `RequestImageModelsMessage` (webview→extension) + `ImageModelsLoadedMessage` (extension→webview). +- [x] i18n strings added to all 20 locales (`imageGeneration.*` + `imageGenerationModel.*`). + +### 7. Tests (TDD) +- [x] `packages/opencode/test/kilocode/tool/generate-image.test.ts` — 21 tests: response parser (data-URL extraction, format detection, edge cases), provider resolver (kilo-auth/api-key/BYO-key/none/preference), path extension logic, model catalog. +- [x] Updated affected test files for `image` tool field + `Auth.defaultLayer` dependency: `tool-registry-indexing.test.ts`, `tool-registry-indexing-import-failure.test.ts`, `tool-registry-semantic-import-failure.test.ts`, `registry.test.ts`, `prompt.test.ts`, `snapshot-tool-race.test.ts`, `session-compaction-cap.test.ts`, `session-prompt-compaction-safety.test.ts`, `session-prompt-permission-refresh.test.ts`. + +### 8. Changeset +- [x] `.changeset/image-generation.md` — `minor` for `kilo-code`. + +## Remaining Tasks + +### CI / Contributing compliance +- [ ] **SDK regen**: `./script/generate.ts` — the new `/kilo/models/images` endpoint was added to the HttpApiGroup; regenerate SDK types. +- [ ] **Source links**: `bun run script/extract-source-links.ts` — new files under `packages/opencode/src/` and `packages/kilo-vscode/`. +- [ ] **opencode annotation check**: `bun run script/check-opencode-annotations.ts` — verify markers on shared file edits. +- [ ] **Cloud config-schema mirror**: add `image_generation` + `image_generation_model` to `apps/web/src/app/config.json/extras.ts` in the [cloud repo](https://github.com/Kilo-Org/cloud). +- [ ] **Issue-first**: create/link a feature issue in the PR. + +### Manual verification (before PR) +- [ ] Confirm the model dropdown populates in VS Code (requires Kilo login + running `kilo serve`). +- [ ] Confirm an actual image generation call succeeds end-to-end (prompt → image file on disk + inline render). +- [ ] Confirm image editing works (input image → transformed output). +- [ ] Confirm the BYO OpenRouter key path works with `OPENROUTER_API_KEY` set. +- [ ] Confirm the tool does NOT appear when the flag is off. +- [ ] Verify TUI behavior: inline image render may degrade to a path note. + +## Risks & Assumptions + +- **Cloud backend dependency:** The Kilo cloud must support chat-completions-with-modalities at `${KILO_API_BASE}/api/openrouter/chat/completions`. The BYO-OpenRouter-key path works regardless. +- **TUI image rendering:** Unverified whether the TUI renders `FilePart` image attachments inline. The file write + path output guarantees correctness regardless. +- **Cost:** Image generation is a paid external call. The opt-in flag (off by default) is the primary guard. + +## Architecture (as implemented) + +``` +User enables flag (VS Code settings or kilo.json) + │ + ├── VS Code: toggle writes experimental.image_generation to config + │ model dropdown fetches GET /kilo/models/images → fetchKiloImageModels() + │ + └── CLI: reads experimental.image_generation from config + +LLM calls generate_image tool + │ + ├── resolveProvider(): Auth.Service → Kilo token? → Kilo cloud URL + │ else OPENROUTER_API_KEY → openrouter.ai URL + │ + ├── fetch chat-completions with modalities:["image","text"] + │ (+ optional input image as base64 data URL for editing) + │ + ├── parse response → choices[0].message.images[0].image_url.url (data URL) + │ + ├── write image bytes to disk (AppFileSystem.writeWithDirs) + │ + └── return { output: path, attachments: [FilePart] } +``` + +## Files Changed + +**New files (Kilo-owned):** +| File | Purpose | +|---|---| +| `packages/opencode/src/kilocode/tool/generate-image.ts` | Tool implementation | +| `packages/opencode/src/kilocode/tool/generate-image.txt` | Tool description | +| `packages/opencode/test/kilocode/tool/generate-image.test.ts` | TDD tests (21) | +| `packages/kilo-vscode/src/image-generation/models.ts` | VS Code fetch helper | +| `packages/kilo-vscode/webview-ui/src/context/image-models.tsx` | Image models context provider | +| `.changeset/image-generation.md` | Changeset | + +**Shared files (with `kilocode_change` markers):** +| File | Change | +|---|---| +| `packages/opencode/src/config/config.ts` | `image_generation` + `image_generation_model` config keys | +| `packages/opencode/src/tool/registry.ts` | `Auth.Service` dependency + layer | + +**Kilo-owned files modified:** +| File | Change | +|---|---| +| `packages/opencode/src/kilocode/tool/registry.ts` | Tool registration + flag gating | +| `packages/opencode/src/kilocode/server/httpapi/groups/kilo-gateway.ts` | `GET /kilo/models/images` endpoint | +| `packages/opencode/src/kilocode/server/httpapi/handlers/kilo-gateway.ts` | `imageModels` handler | +| `packages/kilo-gateway/src/api/models.ts` | `fetchKiloImageModels()` + refactored shared fetch | +| `packages/kilo-gateway/src/server/routes.ts` | Standalone gateway routes | +| `packages/kilo-gateway/src/index.ts` | Exports | +| `packages/kilo-vscode/webview-ui/src/types/messages/config.ts` | Config type | +| `packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts` | `RequestImageModelsMessage` | +| `packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts` | `ImageModelsLoadedMessage` | +| `packages/kilo-vscode/webview-ui/src/components/settings/ExperimentalTab.tsx` | Toggle + dropdown UI | +| `packages/kilo-vscode/webview-ui/src/App.tsx` | Provider registration | +| `packages/kilo-vscode/src/KiloProvider.ts` | Handler + cache | +| `packages/kilo-vscode/webview-ui/src/i18n/*.ts` | 20 locale files | +| 9 test files | `Auth.defaultLayer` + `image` tool field | diff --git a/packages/kilo-docs/source-links.md b/packages/kilo-docs/source-links.md index ce7301c2770..ab92fd63ef6 100644 --- a/packages/kilo-docs/source-links.md +++ b/packages/kilo-docs/source-links.md @@ -11,6 +11,7 @@ - + - - @@ -148,6 +149,8 @@ - +- + - - diff --git a/packages/kilo-gateway/src/api/models.ts b/packages/kilo-gateway/src/api/models.ts index 598255e66a0..a3ffe2b9926 100644 --- a/packages/kilo-gateway/src/api/models.ts +++ b/packages/kilo-gateway/src/api/models.ts @@ -84,6 +84,75 @@ export async function fetchKiloModels(options?: { kilocodeOrganizationId?: string baseURL?: string }): Promise { + const raw = await fetchRawKiloModels(options) + if (raw.error) return { models: {}, error: raw.error } + + // Transform models to ModelsDev.Model format + const models: Record = {} + + for (const model of raw.data) { + // Skip image generation models + if (model.architecture?.output_modalities?.includes("image")) { + continue + } + + // Skip models that don't support tools — Kilo requires tool calling + if (!model.supported_parameters?.includes("tools")) { + continue + } + + const transformedModel = transformToModelDevFormat(model) + models[model.id] = transformedModel + } + + return { models } +} + +export type KiloImageModel = { + id: string + name: string + description?: string +} + +export type KiloImageModelsResult = { + models: KiloImageModel[] + error?: { kind: "unauthorized" | "network" | "schema" | "http"; status?: number } +} + +/** + * Fetch image-capable models from Kilo API (OpenRouter-compatible endpoint). + * Uses the same raw fetch as {@link fetchKiloModels} but keeps only models + * whose `output_modalities` include `"image"`. + */ +export async function fetchKiloImageModels(options?: { + kilocodeToken?: string + kilocodeOrganizationId?: string + baseURL?: string +}): Promise { + const raw = await fetchRawKiloModels(options) + if (raw.error) return { models: [], error: raw.error } + + const models: KiloImageModel[] = [] + + for (const model of raw.data) { + if (model.architecture?.output_modalities?.includes("image")) { + models.push({ id: model.id, name: model.name, description: model.description }) + } + } + + return { models } +} + +/** + * Shared raw fetch + validate used by both {@link fetchKiloModels} and {@link fetchKiloImageModels}. + */ +async function fetchRawKiloModels(options?: { + kilocodeToken?: string + kilocodeOrganizationId?: string + baseURL?: string +}): Promise< + { data: OpenRouterModel[]; error?: undefined } | { data?: undefined; error: NonNullable } +> { const token = options?.kilocodeToken const organizationId = options?.kilocodeOrganizationId @@ -108,50 +177,32 @@ export async function fetchKiloModels(options?: { }).catch((err: unknown) => err as Error) if (response instanceof Error) { - return { models: {}, error: { kind: "network" } } + return { error: { kind: "network" } } } if (!response.ok) { // 401 with auth credentials: fall back to unauthenticated public endpoint if (response.status === 401 && (token || organizationId)) { - return fetchKiloModels({}) + return fetchRawKiloModels({}) } const kind = response.status === 401 || response.status === 403 ? "unauthorized" : "http" - return { models: {}, error: { kind, status: response.status } } + return { error: { kind, status: response.status } } } const json = await response.json().catch(() => null) if (json === null) { - return { models: {}, error: { kind: "schema" } } + return { error: { kind: "schema" } } } // Validate response schema const result = openRouterModelsResponseSchema.safeParse(json) if (!result.success) { - return { models: {}, error: { kind: "schema" } } + return { error: { kind: "schema" } } } - // Transform models to ModelsDev.Model format - const models: Record = {} - - for (const model of result.data.data) { - // Skip image generation models - if (model.architecture?.output_modalities?.includes("image")) { - continue - } - - // Skip models that don't support tools — Kilo requires tool calling - if (!model.supported_parameters?.includes("tools")) { - continue - } - - const transformedModel = transformToModelDevFormat(model) - models[model.id] = transformedModel - } - - return { models } + return { data: result.data.data } } /** diff --git a/packages/kilo-gateway/src/index.ts b/packages/kilo-gateway/src/index.ts index bc8d21df72e..48e27e2e1e7 100644 --- a/packages/kilo-gateway/src/index.ts +++ b/packages/kilo-gateway/src/index.ts @@ -33,7 +33,13 @@ export { getKiloDefaultModel, promptOrganizationSelection, } from "./api/profile.js" -export { fetchKiloModels, type KiloModelsResult } from "./api/models.js" +export { + fetchKiloModels, + type KiloModelsResult, + fetchKiloImageModels, + type KiloImageModel, + type KiloImageModelsResult, +} from "./api/models.js" export { EMPTY_KILO_EMBEDDING_MODEL_CATALOG, fetchKiloEmbeddingModelCatalog, diff --git a/packages/kilo-gateway/src/server/routes.ts b/packages/kilo-gateway/src/server/routes.ts index c90c4f1253b..1d3cef870d8 100644 --- a/packages/kilo-gateway/src/server/routes.ts +++ b/packages/kilo-gateway/src/server/routes.ts @@ -6,6 +6,7 @@ */ import { fetchKilocodeNotifications, KilocodeNotificationSchema } from "../api/notifications.js" +import { fetchKiloImageModels } from "../api/models.js" import { fetchOrganizationModes, clearModesCache } from "../api/modes.js" import { KILO_API_BASE, HEADER_FEATURE, HEADER_ORGANIZATIONID } from "../api/constants.js" import { buildKiloHeaders } from "../headers.js" @@ -435,6 +436,93 @@ export function createKiloRoutes(deps: KiloRoutesDeps) { }) }, ) + .get( + "/models/images", + describeRoute({ + summary: "Image generation models", + description: "List image-capable models from the Kilo Gateway OpenRouter passthrough", + operationId: "kilo.models.images", + responses: { + 200: { + description: "Image model list", + content: { + "application/json": { + schema: resolver( + z.array(z.object({ id: z.string(), name: z.string(), description: z.string().optional() })), + ), + }, + }, + }, + ...errors(401), + }, + }), + async (c: any) => { + const proxy = await getProxyAuth() + if (!proxy.auth) return c.json({ error: "Not authenticated with Kilo Gateway" }, 401) + if (!proxy.token) return c.json({ error: "No valid token found" }, 401) + + const result = await fetchKiloImageModels({ + kilocodeToken: proxy.token, + kilocodeOrganizationId: proxy.organizationId, + }) + + if (result.error) { + const status = result.error.kind === "unauthorized" ? 401 : (result.error.status ?? 500) + return c.json({ error: result.error.kind }, status) + } + + return c.json(result.models) + }, + ) + .post( + "/image/generations", + describeRoute({ + summary: "Image generation", + description: + "Proxy an image generation request (chat-completions with modalities) to the Kilo Gateway OpenRouter passthrough", + operationId: "kilo.image.generations", + responses: { + 200: { + description: "Image generation response", + content: { + "application/json": { + schema: resolver(z.unknown()), + }, + }, + }, + ...errors(400, 401), + }, + }), + validator("json", z.object({ body: z.unknown() }).passthrough()), + async (c: any) => { + const proxy = await getProxyAuth() + if (!proxy.auth) return c.json({ error: "Not authenticated with Kilo Gateway" }, 401) + if (!proxy.token) return c.json({ error: "No valid token found" }, 401) + + const payload = c.req.valid("json") + const headers = { + "Content-Type": "application/json", + Authorization: `Bearer ${proxy.token}`, + ...buildKiloHeaders(undefined, { kilocodeOrganizationId: proxy.organizationId }), + [HEADER_FEATURE]: "vscode-extension", + } + + const response = await fetch(`${KILO_API_BASE}/api/openrouter/chat/completions`, { + method: "POST", + headers, + signal: c.req.raw.signal, + body: JSON.stringify(payload.body ?? payload), + }) + + const text = await response.text() + return new Response(text, { + status: response.status, + headers: { + "Content-Type": response.headers.get("Content-Type") ?? "application/json", + }, + }) + }, + ) .get( "/notifications", describeRoute({ diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index c3a27446d7e..251bb6e0c52 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -149,6 +149,7 @@ import { configFeatures } from "./features" import { createAutoApproveBridge } from "./kilo-provider/auto-approve" import type { KiloProviderOptions } from "./kilo-provider/options" import { fetchKiloEmbeddingModelCatalog } from "@kilocode/kilo-gateway" +import { fetchImageModels } from "./image-generation/models" import { stopSessionProcesses } from "./kilo-provider/background-process" type MessageLoadMode = "replace" | "prepend" | "focus" | "reconcile" @@ -316,6 +317,8 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper private cachedIndexingStatusMessage: unknown = null /** Cached kiloEmbeddingModelsLoaded payload so requestKiloEmbeddingModels is resilient offline. */ private cachedKiloEmbeddingModelsMessage: unknown = null + /** Cached imageModelsLoaded payload so requestImageModels is resilient offline. */ + private cachedImageModelsMessage: unknown = null /** Cached mcpStatusLoaded payload so requestMcpStatus can be served before client is ready */ private cachedMcpStatusMessage: unknown = null /** Ref-count of in-flight handleUpdateConfig calls; prevents fetchAndSendConfig from sending stale data */ @@ -1085,6 +1088,9 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper console.error("[Kilo New] fetchAndSendKiloEmbeddingModels failed:", e), ) break + case "requestImageModels": + this.fetchAndSendImageModels().catch((e) => console.error("[Kilo New] fetchAndSendImageModels failed:", e)) + break case "updateConfig": await this.handleUpdateConfig( message.config, @@ -2254,6 +2260,16 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper this.postMessage(message) } + private async fetchAndSendImageModels(): Promise { + const dir = this.getWorkspaceDirectory() + const result = await fetchImageModels(this.connectionService, dir) + const message = result.ok + ? { type: "imageModelsLoaded" as const, models: result.models } + : { type: "imageModelsLoaded" as const, models: [] } + this.cachedImageModelsMessage = message + this.postMessage(message) + } + /** * Seed sessionStatusMap with current session statuses on connect. * Without this, the Settings panel (which has no tracked sessions) would see diff --git a/packages/kilo-vscode/src/image-generation/models.ts b/packages/kilo-vscode/src/image-generation/models.ts new file mode 100644 index 00000000000..0fdefc76d09 --- /dev/null +++ b/packages/kilo-vscode/src/image-generation/models.ts @@ -0,0 +1,41 @@ +import type { KiloConnectionService } from "../services/cli-backend/connection-service" +import { getErrorMessage } from "../kilo-provider-utils" + +const PATH = "/kilo/models/images" + +export type ImageModel = { + id: string + name: string + description?: string +} + +export type ImageModelsResult = { ok: true; models: ImageModel[] } | { ok: false; error: string } + +export async function fetchImageModels( + connection: KiloConnectionService, + dir: string, + signal?: AbortSignal, +): Promise { + const cfg = connection.getServerConfig() + if (!cfg) return { ok: false, error: "Not connected to the Kilo backend" } + + const auth = Buffer.from(`kilo:${cfg.password}`).toString("base64") + const url = new URL(PATH, cfg.baseUrl) + if (dir) url.searchParams.set("directory", dir) + + try { + const res = await fetch(url, { + signal, + headers: { Authorization: `Basic ${auth}` }, + }) + + if (!res.ok) { + return { ok: false, error: `Failed to fetch image models (HTTP ${res.status})` } + } + + const models = (await res.json()) as ImageModel[] + return { ok: true, models } + } catch (err) { + return { ok: false, error: getErrorMessage(err) } + } +} diff --git a/packages/kilo-vscode/webview-ui/src/App.tsx b/packages/kilo-vscode/webview-ui/src/App.tsx index de57030df63..75b0f875d72 100644 --- a/packages/kilo-vscode/webview-ui/src/App.tsx +++ b/packages/kilo-vscode/webview-ui/src/App.tsx @@ -36,6 +36,7 @@ import { MigrationWizard } from "./components/migration" // legacy-migration import { NotificationsProvider } from "./context/notifications" import { FeedbackProvider } from "./context/feedback" import { KiloEmbeddingModelsProvider } from "./context/kilo-embedding-models" +import { ImageModelsProvider } from "./context/image-models" import type { Message as SDKMessage, Part as SDKPart } from "@kilocode/sdk/v2" import "./styles/chat.css" @@ -382,15 +383,17 @@ const App: Component = () => { - - - - - - - - - + + + + + + + + + + + diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/ExperimentalTab.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/ExperimentalTab.tsx index 30c240b4f97..706fa8fd990 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/ExperimentalTab.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/ExperimentalTab.tsx @@ -6,6 +6,7 @@ import { Card } from "@kilocode/kilo-ui/card" import { useConfig } from "../../context/config" import { useLanguage } from "../../context/language" import { useVSCode } from "../../context/vscode" +import { useImageModels } from "../../context/image-models" import type { ExtensionMessage } from "../../types/messages" import SettingsRow from "./SettingsRow" @@ -23,6 +24,7 @@ const SHARE_OPTIONS: ShareOption[] = [ const ExperimentalTab: Component = () => { const { config, updateConfig } = useConfig() const language = useLanguage() + const imageModels = useImageModels() const vscode = useVSCode() const [active, setActive] = createSignal(false) @@ -154,6 +156,41 @@ const ExperimentalTab: Component = () => { + + updateExperimental("image_generation", checked)} + hideLabel + > + {language.t("settings.experimental.imageGeneration.title")} + + + + + +