From 376398e887cd9c1789c6e25d59c5dee732f062c1 Mon Sep 17 00:00:00 2001 From: markijbema <624143+markijbema@users.noreply.github.com> Date: Thu, 25 Jun 2026 11:19:13 +0000 Subject: [PATCH 1/5] ci(github): implement path filtering to skip unit tests for JetBrains-only changes Add a `changes` job that uses `paths-filter` to detect if changes are restricted to the `packages/kilo-jetbrains/` directory. The `unit` job is updated to depend on this job and uses a dynamic matrix to skip execution on non-JetBrains platforms when only JetBrains files are modified, optimizing CI runtime. Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 66 ++++++++++++++++++++++++++++++-------- 1 file changed, 53 insertions(+), 13 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dbb18d8a4d7..9577cf204dc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,31 +22,67 @@ env: jobs: # kilocode_change start + changes: + name: detect non-JetBrains changes + runs-on: blacksmith-4vcpu-ubuntu-2404 + permissions: + contents: read + pull-requests: read + outputs: + settings: ${{ steps.matrix.outputs.settings }} + steps: + - name: Checkout repository + if: github.event_name != 'workflow_dispatch' + uses: actions/checkout@v6 + + - name: Detect non-JetBrains changes + if: github.event_name != 'workflow_dispatch' + id: filter + uses: Kilo-Org/paths-filter@668c092af3649c4b664c54e4b704aa46782f6f7c # v3 + with: + predicate-quantifier: every + filters: | + general: + - '**' + - '!packages/kilo-jetbrains/**' + + - name: Configure unit matrix + id: matrix + env: + GENERAL: ${{ github.event_name == 'workflow_dispatch' || steps.filter.outputs.general == 'true' }} + run: | + if [ "$GENERAL" = "true" ]; then + echo 'settings=[{"name":"linux","host":"blacksmith-4vcpu-ubuntu-2404","run":true},{"name":"macos","host":"macos-15","run":true},{"name":"windows","host":"blacksmith-4vcpu-windows-2025","run":true}]' >> "$GITHUB_OUTPUT" + exit 0 + fi + echo 'settings=[{"name":"linux","host":"blacksmith-4vcpu-ubuntu-2404","run":false}]' >> "$GITHUB_OUTPUT" + unit: name: unit (${{ matrix.settings.name }}) + needs: changes strategy: fail-fast: false matrix: - settings: - - name: linux - host: blacksmith-4vcpu-ubuntu-2404 # kilocode_change - - name: macos - host: macos-15 # kilocode_change - - name: windows - host: blacksmith-4vcpu-windows-2025 # kilocode_change + settings: ${{ fromJSON(needs.changes.outputs.settings) }} runs-on: ${{ matrix.settings.host }} timeout-minutes: 45 # kilocode_change defaults: run: shell: bash steps: + - name: Skip unchanged general unit tests + if: ${{ !matrix.settings.run }} + run: echo "Only JetBrains files changed; general unit tests are unchanged." + - name: Checkout repository + if: matrix.settings.run uses: actions/checkout@v6 # kilocode_change with: token: ${{ secrets.GITHUB_TOKEN }} # kilocode_change start - name: Setup Node + if: matrix.settings.run id: setup-node continue-on-error: ${{ runner.os == 'Windows' }} uses: actions/setup-node@v6 # kilocode_change @@ -54,18 +90,19 @@ jobs: node-version: "24" - name: Retry Setup Node on Windows - if: runner.os == 'Windows' && steps.setup-node.outcome == 'failure' + if: matrix.settings.run && runner.os == 'Windows' && steps.setup-node.outcome == 'failure' uses: actions/setup-node@v6 with: node-version: "24" # kilocode_change end - name: Setup Bun + if: matrix.settings.run uses: ./.github/actions/setup-bun # kilocode_change start - name: Setup Zig for Linux sandbox helper - if: runner.os == 'Linux' + if: matrix.settings.run && runner.os == 'Linux' run: | curl --fail --location --retry 3 \ https://ziglang.org/download/0.14.0/zig-linux-x86_64-0.14.0.tar.xz \ @@ -75,17 +112,19 @@ jobs: echo "$RUNNER_TEMP/zig-linux-x86_64-0.14.0" >> "$GITHUB_PATH" - name: Build Linux sandbox helper - if: runner.os == 'Linux' + if: matrix.settings.run && runner.os == 'Linux' run: | bun packages/opencode/script/kilocode/bubblewrap.ts --arch x64 --output "$RUNNER_TEMP/bwrap" echo "KILO_BWRAP_PATH=$RUNNER_TEMP/bwrap" >> "$GITHUB_ENV" # kilocode_change end - name: Configure git identity + if: matrix.settings.run run: | git config --global user.email "kilo-maintainer[bot]@users.noreply.github.com" git config --global user.name "kilo-maintainer[bot]" - name: Cache Turbo + if: matrix.settings.run uses: actions/cache@v5 # kilocode_change with: path: node_modules/.cache/turbo @@ -95,6 +134,7 @@ jobs: turbo-${{ runner.os }}- - name: Run unit tests + if: matrix.settings.run run: bun turbo test:ci --filter='!@kilocode/kilo-jetbrains' env: KILO_EXPERIMENTAL_DISABLE_FILEWATCHER: ${{ runner.os == 'Windows' && 'true' || 'false' }} @@ -102,13 +142,13 @@ jobs: # kilocode_change start - name: Run HttpApi exerciser gates - if: runner.os == 'Linux' + if: matrix.settings.run && runner.os == 'Linux' working-directory: packages/opencode run: bun run test:httpapi # kilocode_change end - name: Publish unit reports # kilocode_change - if: always() + if: always() && matrix.settings.run uses: mikepenz/action-junit-report@bccf2e31636835cf0874589931c4116687171386 # v6.4.0 with: report_paths: packages/*/.artifacts/unit/junit.xml @@ -118,7 +158,7 @@ jobs: fail_on_failure: false - name: Upload unit artifacts - if: always() + if: always() && matrix.settings.run uses: actions/upload-artifact@v7 # kilocode_change with: name: unit-${{ matrix.settings.name }}-${{ github.run_attempt }} From 8d449df39bd6fde3a70bcc43d2be70774dfe9ae9 Mon Sep 17 00:00:00 2001 From: markijbema <624143+markijbema@users.noreply.github.com> Date: Thu, 25 Jun 2026 12:59:26 +0000 Subject: [PATCH 2/5] fix(ci): retain full tests outside pull requests Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9577cf204dc..7d467cadb9b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -49,7 +49,7 @@ jobs: - name: Configure unit matrix id: matrix env: - GENERAL: ${{ github.event_name == 'workflow_dispatch' || steps.filter.outputs.general == 'true' }} + GENERAL: ${{ github.event_name != 'pull_request' || steps.filter.outputs.general == 'true' }} run: | if [ "$GENERAL" = "true" ]; then echo 'settings=[{"name":"linux","host":"blacksmith-4vcpu-ubuntu-2404","run":true},{"name":"macos","host":"macos-15","run":true},{"name":"windows","host":"blacksmith-4vcpu-windows-2025","run":true}]' >> "$GITHUB_OUTPUT" From 7b1383751fa3241e195a3a3e5d6ef15b7127b714 Mon Sep 17 00:00:00 2001 From: markijbema <624143+markijbema@users.noreply.github.com> Date: Fri, 26 Jun 2026 09:42:10 +0000 Subject: [PATCH 3/5] ci: skip general tests for isolated changes Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7d467cadb9b..c06eda17a87 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -23,7 +23,7 @@ env: jobs: # kilocode_change start changes: - name: detect non-JetBrains changes + name: detect general unit test changes runs-on: blacksmith-4vcpu-ubuntu-2404 permissions: contents: read @@ -35,7 +35,7 @@ jobs: if: github.event_name != 'workflow_dispatch' uses: actions/checkout@v6 - - name: Detect non-JetBrains changes + - name: Detect general unit test changes if: github.event_name != 'workflow_dispatch' id: filter uses: Kilo-Org/paths-filter@668c092af3649c4b664c54e4b704aa46782f6f7c # v3 @@ -44,7 +44,31 @@ jobs: filters: | general: - '**' + - '!.changeset/**' - '!packages/kilo-jetbrains/**' + - '!packages/kilo-vscode/**' + - '!packages/kilo-docs/**' + - '!packages/extensions/zed/**' + - '!specs/**' + - '!perf/**' + - '!.github/ISSUE_TEMPLATE/**' + - '!.github/CODEOWNERS' + - '!.github/pull_request_template.md' + - '!.github/workflows/disabled/**' + - '!.idea/**' + - '!.vscode/**' + - '!.zed/**' + - '!kilocode-2.code-workspace' + - '!README*.md' + - '!AGENTS.md' + - '!CONTRIBUTING.md' + - '!PRIVACY.md' + - '!REVIEW.md' + - '!SECURITY.md' + - '!TESTING.md' + - '!LICENSE' + - '!logo.png' + - '!screenshot-uk.png' - name: Configure unit matrix id: matrix @@ -72,7 +96,7 @@ jobs: steps: - name: Skip unchanged general unit tests if: ${{ !matrix.settings.run }} - run: echo "Only JetBrains files changed; general unit tests are unchanged." + run: echo "Only isolated product, documentation, or metadata files changed; general unit tests are unchanged." - name: Checkout repository if: matrix.settings.run From ef3e8cbffd88dfa6eab3163dbd7ccf5d7af6b792 Mon Sep 17 00:00:00 2001 From: Mark IJbema Date: Fri, 26 Jun 2026 15:36:08 +0200 Subject: [PATCH 4/5] remove overly specific ones --- .github/workflows/test.yml | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c06eda17a87..96c15f41d0b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -51,24 +51,6 @@ jobs: - '!packages/extensions/zed/**' - '!specs/**' - '!perf/**' - - '!.github/ISSUE_TEMPLATE/**' - - '!.github/CODEOWNERS' - - '!.github/pull_request_template.md' - - '!.github/workflows/disabled/**' - - '!.idea/**' - - '!.vscode/**' - - '!.zed/**' - - '!kilocode-2.code-workspace' - - '!README*.md' - - '!AGENTS.md' - - '!CONTRIBUTING.md' - - '!PRIVACY.md' - - '!REVIEW.md' - - '!SECURITY.md' - - '!TESTING.md' - - '!LICENSE' - - '!logo.png' - - '!screenshot-uk.png' - name: Configure unit matrix id: matrix From 9394887e1344eb1333ad2a60ec7c974a1cad8eec Mon Sep 17 00:00:00 2001 From: Johnny Eric Amancio Date: Fri, 26 Jun 2026 20:17:21 +0200 Subject: [PATCH 5/5] feat(vscode): improve auto model picker details (#11724) * feat(vscode): improve auto model picker details * fix(vscode): address auto picker review feedback --- .changeset/auto-model-picker-details.md | 5 ++ packages/kilo-gateway/src/api/models.ts | 7 +++ packages/kilo-gateway/test/api/models.test.ts | 39 +++++++++++++ .../model-selector-accessibility.spec.ts | 56 ++++++++++++++---- .../tests/unit/model-selector-utils.test.ts | 57 +++++++++++++++++++ .../src/components/shared/ModelPreview.tsx | 39 ++++++++++++- .../src/components/shared/ModelSelector.tsx | 38 +++++++++++-- .../components/shared/model-selector-utils.ts | 34 +++++++++++ .../kilo-vscode/webview-ui/src/i18n/ar.ts | 2 + .../kilo-vscode/webview-ui/src/i18n/br.ts | 2 + .../kilo-vscode/webview-ui/src/i18n/bs.ts | 2 + .../kilo-vscode/webview-ui/src/i18n/da.ts | 2 + .../kilo-vscode/webview-ui/src/i18n/de.ts | 2 + .../kilo-vscode/webview-ui/src/i18n/en.ts | 2 + .../kilo-vscode/webview-ui/src/i18n/es.ts | 2 + .../kilo-vscode/webview-ui/src/i18n/fr.ts | 2 + .../kilo-vscode/webview-ui/src/i18n/it.ts | 2 + .../kilo-vscode/webview-ui/src/i18n/ja.ts | 2 + .../kilo-vscode/webview-ui/src/i18n/ko.ts | 2 + .../kilo-vscode/webview-ui/src/i18n/nl.ts | 2 + .../kilo-vscode/webview-ui/src/i18n/no.ts | 2 + .../kilo-vscode/webview-ui/src/i18n/pl.ts | 2 + .../kilo-vscode/webview-ui/src/i18n/ru.ts | 2 + .../kilo-vscode/webview-ui/src/i18n/th.ts | 2 + .../kilo-vscode/webview-ui/src/i18n/tr.ts | 2 + .../kilo-vscode/webview-ui/src/i18n/uk.ts | 2 + .../kilo-vscode/webview-ui/src/i18n/zh.ts | 2 + .../kilo-vscode/webview-ui/src/i18n/zht.ts | 2 + .../webview-ui/src/stories/shared.stories.tsx | 13 +++++ .../webview-ui/src/styles/model-selector.css | 53 +++++++++++++++++ .../src/types/messages/providers.ts | 1 + .../src/kilocode/provider/provider.ts | 7 +++ .../kilocode/provider-model-metadata.test.ts | 45 +++++++++++++++ packages/sdk/js/src/v2/gen/types.gen.ts | 3 + packages/sdk/openapi.json | 13 +++++ 35 files changed, 434 insertions(+), 16 deletions(-) create mode 100644 .changeset/auto-model-picker-details.md create mode 100644 packages/opencode/test/kilocode/provider-model-metadata.test.ts diff --git a/.changeset/auto-model-picker-details.md b/.changeset/auto-model-picker-details.md new file mode 100644 index 00000000000..78dd225b9b2 --- /dev/null +++ b/.changeset/auto-model-picker-details.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Open the model picker expanded by default, keep the preview pinned while hovering, separate Kilo Auto models into their own marked group with summary tooltips, and show backend-provided Auto Efficient model choices. diff --git a/packages/kilo-gateway/src/api/models.ts b/packages/kilo-gateway/src/api/models.ts index 598255e66a0..5ee3a633528 100644 --- a/packages/kilo-gateway/src/api/models.ts +++ b/packages/kilo-gateway/src/api/models.ts @@ -38,6 +38,12 @@ const openRouterModelSchema = z.object({ isFree: z.boolean().optional(), mayTrainOnYourPrompts: z.boolean().optional(), hasUserByokAvailable: z.boolean().optional(), + autoRouting: z + .object({ + models: z.array(z.string()), + }) + .optional() + .catch(undefined), terminalBench: z .object({ overallScore: z.number(), @@ -194,6 +200,7 @@ function transformToModelDevFormat(model: OpenRouterModel): any { isFree: model.isFree, mayTrainOnYourPrompts: model.mayTrainOnYourPrompts, hasUserByokAvailable: model.hasUserByokAvailable, + ...(model.autoRouting && { autoRouting: model.autoRouting }), ...(model.terminalBench && { terminalBench: model.terminalBench }), ...(inputPrice !== undefined && outputPrice !== undefined && { diff --git a/packages/kilo-gateway/test/api/models.test.ts b/packages/kilo-gateway/test/api/models.test.ts index 56a589af0e5..9bf34793b16 100644 --- a/packages/kilo-gateway/test/api/models.test.ts +++ b/packages/kilo-gateway/test/api/models.test.ts @@ -42,6 +42,25 @@ const VALID_BENCH_RESPONSE = JSON.stringify({ ], }) +const VALID_AUTO_ROUTING_RESPONSE = JSON.stringify({ + data: [ + { + id: "kilo-auto/efficient", + name: "Kilo Auto Efficient", + context_length: 128000, + max_completion_tokens: 16384, + architecture: { + input_modalities: ["text"], + output_modalities: ["text"], + }, + supported_parameters: ["tools", "temperature"], + autoRouting: { + models: ["google/gemini-2.5-flash", "anthropic/claude-sonnet-4.6"], + }, + }, + ], +}) + const INVALID_BENCH_RESPONSE = JSON.stringify({ data: [ { @@ -174,6 +193,26 @@ test("preserves Terminal Bench metadata as a dedicated model field", async () => }) }) +test("preserves Auto Efficient routing metadata as a dedicated model field", async () => { + const orig = globalThis.fetch + stubFetch( + async () => + new Response(VALID_AUTO_ROUTING_RESPONSE, { + status: 200, + headers: { "content-type": "application/json" }, + }), + ) + + const result = await fetchKiloModels({}) + + ;(globalThis as any).fetch = orig + + expect(result.error).toBeUndefined() + expect(result.models["kilo-auto/efficient"].autoRouting).toEqual({ + models: ["google/gemini-2.5-flash", "anthropic/claude-sonnet-4.6"], + }) +}) + test("omits malformed Terminal Bench metadata without rejecting the catalog", async () => { const orig = globalThis.fetch stubFetch( diff --git a/packages/kilo-vscode/tests/model-selector-accessibility.spec.ts b/packages/kilo-vscode/tests/model-selector-accessibility.spec.ts index 03ae6498bae..527380a8803 100644 --- a/packages/kilo-vscode/tests/model-selector-accessibility.spec.ts +++ b/packages/kilo-vscode/tests/model-selector-accessibility.spec.ts @@ -28,23 +28,56 @@ test("model selector exposes combobox relationships and active option movement", await expect(combobox).toHaveAccessibleDescription("Choose the model used for code review tasks.") await expect(alpha.locator("button")).toHaveCount(0) await expect(page.getByRole("button", { name: "Add to favorites: Alpha" })).toBeVisible() + await expect(page.locator(".model-selector-group-label").nth(0)).toContainText("Auto Models") + await expect(page.locator(".model-selector-group-label").nth(1)).toContainText("Recommended") + const auto = page.getByRole("treeitem", { name: /Kilo Auto Efficient/ }) + await expect(auto).toBeVisible() + await expect(auto.locator(".model-selector-auto-icon")).toHaveAttribute( + "aria-label", + "Routes each request to the cheapest model that gets the job done, based on continuously benchmarked accuracy and cost.", + ) + await expect(page.getByRole("treeitem", { name: "Omega" })).toBeVisible() await combobox.press("ArrowDown") await expect(combobox).toBeFocused() await expect(combobox).toHaveAttribute("aria-activedescendant", await bravo.getAttribute("id")) - const expand = page.getByRole("button", { name: "Expand" }) - const controls = await expand.getAttribute("aria-controls") - const preview = page.locator(`[id="${controls}"]`) - await expect(expand).toHaveAttribute("aria-expanded", "false") - await expect(preview).toHaveAttribute("aria-hidden", "true") - await expect(preview.locator("button, a, [tabindex]")).toHaveCount(0) - await expand.click() const collapse = page.getByRole("button", { name: "Collapse", exact: true }) - await expect(collapse).toHaveAttribute("aria-controls", controls!) + const controls = await collapse.getAttribute("aria-controls") + const preview = page.locator(`[id="${controls}"]`) await expect(collapse).toHaveAttribute("aria-expanded", "true") await expect(preview).toHaveAttribute("aria-hidden", "false") await expect(preview.getByRole("button", { name: "Add to favorites" })).toBeVisible() + const listBeforePreview = await tree.evaluate((el, id) => { + const node = document.getElementById(id!) + return !!node && !!(el.compareDocumentPosition(node) & Node.DOCUMENT_POSITION_FOLLOWING) + }, controls) + const treeBox = await tree.boundingBox() + const previewBox = await preview.boundingBox() + expect(listBeforePreview).toBe(true) + expect(previewBox!.y).toBeLessThan(treeBox!.y) + await collapse.click() + const expand = page.getByRole("button", { name: "Expand" }) + await expect(expand).toHaveAttribute("aria-controls", controls!) + await expect(expand).toHaveAttribute("aria-expanded", "false") + await expect(preview).toHaveAttribute("aria-hidden", "true") + await expect(preview.locator("button, a, [tabindex]")).toHaveCount(0) +}) + +test("auto efficient details show server description and model choices", async ({ page }) => { + await load(page, "shared--model-selector-accessible") + + await page.getByRole("button", { name: "Review model: Alpha" }).click() + await page.getByRole("treeitem", { name: /Kilo Auto Efficient/ }).click() + + const preview = page.locator(".model-selector-preview") + await expect(preview).toContainText( + "Routes each request to the cheapest model that gets the job done, based on continuously benchmarked accuracy and cost.", + ) + await expect(preview).toContainText("Model choices") + await expect(preview).toContainText("google/gemini-2.5-flash") + await expect(preview).toContainText("anthropic/claude-sonnet-4.6") + await expect(preview).not.toContainText("openai/gpt-5.5") }) test("typing a provider initial moves the active descendant to matching results", async ({ page }) => { @@ -65,7 +98,7 @@ test("provider groups collapse, expand, and skip their model rows", async ({ pag await page.getByRole("button", { name: "Review model: Alpha" }).click() const combobox = page.getByRole("combobox", { name: "Review model: Alpha. Search models" }) - const kilo = page.getByRole("treeitem", { name: "Kilo" }) + const kilo = page.getByRole("treeitem", { name: "Kilo", exact: true }) const nvidia = page.getByRole("treeitem", { name: "NVIDIA" }) await combobox.press("ArrowDown") @@ -120,7 +153,10 @@ test("expanded preview waits for explicit pointer selection", async ({ page }) = await load(page, "shared--model-selector-accessible") await page.getByRole("button", { name: "Review model: Alpha" }).click() - await page.getByRole("button", { name: "Expand" }).click() + await expect(page.locator(".model-selector-preview")).toContainText("Alpha") + await page.getByRole("treeitem", { name: "Bravo" }).hover() + await page.waitForTimeout(250) + await expect(page.locator(".model-selector-preview")).toContainText("Alpha") await page.getByRole("treeitem", { name: "Bravo" }).click() await expect(page.getByTestId("model-selector-value")).toHaveText("alpha") diff --git a/packages/kilo-vscode/tests/unit/model-selector-utils.test.ts b/packages/kilo-vscode/tests/unit/model-selector-utils.test.ts index 8fbbd5e51c4..f58b20349bc 100644 --- a/packages/kilo-vscode/tests/unit/model-selector-utils.test.ts +++ b/packages/kilo-vscode/tests/unit/model-selector-utils.test.ts @@ -10,6 +10,9 @@ import { isDataCollectedModel, hasByok, isFree, + isAuto, + autoSummary, + autoChoices, } from "../../webview-ui/src/components/shared/model-selector-utils" const labels = { select: "Select model", noProviders: "No providers", notSet: "Not set" } @@ -112,6 +115,60 @@ describe("isFree", () => { }) }) +describe("isAuto", () => { + it("matches only Kilo Auto model ids", () => { + expect(isAuto({ providerID: KILO_GATEWAY_ID, id: "kilo-auto/efficient" })).toBe(true) + expect(isAuto({ providerID: KILO_GATEWAY_ID, id: "auto-small" })).toBe(true) + expect(isAuto({ providerID: "anthropic", id: "kilo-auto/efficient" })).toBe(false) + expect(isAuto({ providerID: KILO_GATEWAY_ID, id: "anthropic/claude-sonnet" })).toBe(false) + }) +}) + +describe("autoChoices", () => { + it("uses backend Auto Efficient routes and resolves names when available", () => { + expect( + autoChoices( + { + providerID: KILO_GATEWAY_ID, + id: "kilo-auto/efficient", + autoRouting: { models: ["provider/model", "missing/model"] }, + }, + [{ id: "provider/model", name: "Provider: Model" }], + ), + ).toEqual([ + { id: "provider/model", name: "Model" }, + { id: "missing/model", name: "missing/model" }, + ]) + }) + + it("ignores missing routes and non-efficient Auto models", () => { + expect(autoChoices({ providerID: KILO_GATEWAY_ID, id: "kilo-auto/efficient" })).toEqual([]) + expect( + autoChoices({ + providerID: KILO_GATEWAY_ID, + id: "kilo-auto/frontier", + autoRouting: { models: ["provider/model"] }, + }), + ).toEqual([]) + }) +}) + +describe("autoSummary", () => { + it("uses the first description paragraph for compact tooltips", () => { + expect( + autoSummary({ + options: { + description: "Routes through available models.\n\nLong details.", + }, + }), + ).toBe("Routes through available models.") + }) + + it("falls back when there is no description", () => { + expect(autoSummary({})).toBe("Routes requests automatically.") + }) +}) + describe("isDataCollectedModel", () => { it("uses only explicit prompt training metadata", () => { expect(isDataCollectedModel({ mayTrainOnYourPrompts: true })).toBe(true) diff --git a/packages/kilo-vscode/webview-ui/src/components/shared/ModelPreview.tsx b/packages/kilo-vscode/webview-ui/src/components/shared/ModelPreview.tsx index 485d3b80032..b8146eda0e3 100644 --- a/packages/kilo-vscode/webview-ui/src/components/shared/ModelPreview.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/shared/ModelPreview.tsx @@ -5,11 +5,20 @@ import { Markdown } from "@kilocode/kilo-ui/markdown" import { Icon } from "@kilocode/kilo-ui/icon" import { Tooltip } from "@kilocode/kilo-ui/tooltip" import { useLanguage } from "../../context/language" -import { freeDataLabel, hasByok, isDataCollectedModel, sanitizeName } from "./model-selector-utils" +import { + autoChoices, + autoSummary, + freeDataLabel, + hasByok, + isAuto, + isDataCollectedModel, + sanitizeName, +} from "./model-selector-utils" import { avgPrice, fmtAttemptCost, fmtCachedPrice, fmtPrice, fmtTerminalBenchScore } from "./model-preview-utils" interface Props { model: EnrichedModel | null + models?: readonly EnrichedModel[] } function fmtContext(n: number): string { @@ -49,6 +58,9 @@ export const ModelPreview: Component = (props) => { const avg = () => (cost() ? avgPrice(cost()!) : undefined) const freeLabel = () => language.t("model.tag.free") const dataLabel = () => freeDataLabel(language.t("model.tag.free"), language.t("model.tag.dataCollected")) + const autoLabel = () => autoSummary(model()) + const choices = () => autoChoices(model(), props.models ?? []) + const choicesLabel = () => language.t("model.preview.group.autoEfficientChoices") const ctx = () => model().limit?.context ?? model().contextLength const caps = () => model().capabilities const inputs = () => caps()?.input @@ -91,8 +103,15 @@ export const ModelPreview: Component = (props) => { })()} - + + + + + + + + {freeLabel()} @@ -181,6 +200,22 @@ export const ModelPreview: Component = (props) => { + + 0}> +
+ {choicesLabel()} +
+ + {(item) => ( + + {item.name} + {item.id} + + )} + +
+
+
) }} diff --git a/packages/kilo-vscode/webview-ui/src/components/shared/ModelSelector.tsx b/packages/kilo-vscode/webview-ui/src/components/shared/ModelSelector.tsx index e856d5c1f6e..1e44e6115c6 100644 --- a/packages/kilo-vscode/webview-ui/src/components/shared/ModelSelector.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/shared/ModelSelector.tsx @@ -39,7 +39,9 @@ import { isFree, isDataCollectedModel, hasByok, + isAuto, freeDataLabel, + autoSummary, buildTriggerLabel, sanitizeName, } from "./model-selector-utils" @@ -52,6 +54,7 @@ import { searchMatch } from "../../utils/search-match" const CLEAR_KEY = "clear" const FAVORITES_KEY = "favorites" +const AUTO_KEY = "auto" const RECOMMENDED_KEY = "recommended" function modelKey(providerID: string, modelID: string) { @@ -148,7 +151,7 @@ export const ModelSelectorBase: Component = (props) => { } const [open, setOpen] = createSignal(false) - const [expanded, setExpanded] = createSignal(false) + const [expanded, setExpanded] = createSignal(true) const [search, setSearch] = createSignal("") const [debouncedSearch, setDebouncedSearch] = createSignal("") const [selectedKey, setSelectedKey] = createSignal(CLEAR_KEY) @@ -185,7 +188,7 @@ export const ModelSelectorBase: Component = (props) => { function onMove(e: MouseEvent) { if (!body) return - const delta = startY - e.clientY + const delta = e.clientY - startY // Subtract fixed chrome (search wrapper + splitter) so the list always // retains at least 80px, rather than the preview consuming that space. const chrome = (searchWrapperRef?.offsetHeight ?? 0) + (splitterRef?.offsetHeight ?? 0) @@ -260,10 +263,15 @@ export const ModelSelectorBase: Component = (props) => { }) const groups = createMemo(() => { + const autos: EnrichedModel[] = [] const recommended: EnrichedModel[] = [] const map = new Map() for (const m of filtered()) { + if (isAuto(m)) { + autos.push(m) + continue + } if (m.recommendedIndex !== undefined) { recommended.push(m) continue @@ -273,6 +281,9 @@ export const ModelSelectorBase: Component = (props) => { map.set(m.providerID, list) } + autos.sort( + (a, b) => (a.recommendedIndex ?? Infinity) - (b.recommendedIndex ?? Infinity) || a.name.localeCompare(b.name), + ) recommended.sort((a, b) => (a.recommendedIndex ?? Infinity) - (b.recommendedIndex ?? Infinity)) const result: ModelGroup[] = [] @@ -291,6 +302,18 @@ export const ModelSelectorBase: Component = (props) => { }) } + if (autos.length > 0) { + result.push({ + key: AUTO_KEY, + label: language.t("model.group.auto"), + rows: autos.map((m) => ({ + key: rowKey("model", m.providerID, m.id), + kind: "model", + model: m, + })), + }) + } + if (recommended.length > 0) { result.push({ key: RECOMMENDED_KEY, @@ -675,6 +698,7 @@ export const ModelSelectorBase: Component = (props) => { const describedBy = () => (props.description ? descriptionID : undefined) const freeLabel = () => language.t("model.tag.free") const dataLabel = () => freeDataLabel(language.t("model.tag.free"), language.t("model.tag.dataCollected")) + const autoLabel = (model: EnrichedModel) => autoSummary(model) const activeCollectsData = () => { const model = activeModel() if (!model) return false @@ -917,7 +941,6 @@ export const ModelSelectorBase: Component = (props) => { }} onMouseEnter={() => { if (pointer()) setSelectedKey(row.key) - schedulePreview(row.key) }} >
@@ -937,6 +960,13 @@ export const ModelSelectorBase: Component = (props) => { ) })()} + + + + + + + @@ -1011,7 +1041,7 @@ export const ModelSelectorBase: Component = (props) => { style={expanded() ? { height: `${previewHeight()}px` } : {}} > - +
diff --git a/packages/kilo-vscode/webview-ui/src/components/shared/model-selector-utils.ts b/packages/kilo-vscode/webview-ui/src/components/shared/model-selector-utils.ts index 5c75b79782a..4cb59d26c61 100644 --- a/packages/kilo-vscode/webview-ui/src/components/shared/model-selector-utils.ts +++ b/packages/kilo-vscode/webview-ui/src/components/shared/model-selector-utils.ts @@ -9,6 +9,40 @@ import { export { KILO_GATEWAY_ID, PROVIDER_ORDER } export const KILO_AUTO_SMALL_IDS = new Set(["kilo-auto/small", "auto-small"]) +export const KILO_AUTO_EFFICIENT_ID = "kilo-auto/efficient" +const AUTO_FALLBACK = "Routes requests automatically." + +interface Choice { + id: string + name: string +} + +export function isAuto(model: Pick): boolean { + return ( + model.providerID === KILO_GATEWAY_ID && (model.id.startsWith("kilo-auto/") || KILO_AUTO_SMALL_IDS.has(model.id)) + ) +} + +export function isAutoEfficient(model: Pick): boolean { + return model.providerID === KILO_GATEWAY_ID && model.id === KILO_AUTO_EFFICIENT_ID +} + +export function autoChoices( + model: Pick, + catalog: readonly Pick[] = [], +): readonly Choice[] { + if (!isAutoEfficient(model)) return [] + const ids = model.autoRouting?.models + if (!ids?.length) return [] + const names = new Map(catalog.map((item) => [item.id, stripSubProviderPrefix(sanitizeName(item.name))])) + return ids.map((id) => ({ id, name: names.get(id) ?? id })) +} + +export function autoSummary(model: Pick): string { + const raw = model.options?.description?.split(/\n\s*\n/)[0] + if (!raw) return AUTO_FALLBACK + return raw.replace(/\s+/g, " ").trim() || AUTO_FALLBACK +} export function isSmall(model: Pick): boolean { return model.providerID === KILO_GATEWAY_ID && KILO_AUTO_SMALL_IDS.has(model.id) diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts index 4f983fc194c..d0d94f0e521 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts @@ -181,6 +181,7 @@ export const dict = { "model.tag.free": "مجاني", "model.tag.dataCollected": "قد تُستخدم البيانات للتدريب", "model.tag.latest": "الأحدث", + "model.group.auto": "النماذج التلقائية", "model.group.recommended": "موصى به", "model.group.favorites": "المفضلة", "model.group.collapse": "طي {{group}}", @@ -208,6 +209,7 @@ export const dict = { "model.preview.label.average": "متوسط التكلفة التقديرية", "model.preview.label.context": "السياق", "model.preview.group.terminalBench": "Terminal Bench 2.0", + "model.preview.group.autoEfficientChoices": "خيارات النماذج", "model.preview.label.completion": "الإكمال", "model.preview.label.costAttempt": "التكلفة / المحاولة", "model.preview.value.notSupported": "غير مدعوم", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/br.ts b/packages/kilo-vscode/webview-ui/src/i18n/br.ts index 981f18427ff..e7e79d59a09 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/br.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/br.ts @@ -182,6 +182,7 @@ export const dict = { "model.tag.free": "Grátis", "model.tag.dataCollected": "Os dados podem ser usados para treinamento", "model.tag.latest": "Mais recente", + "model.group.auto": "Modelos automáticos", "model.group.recommended": "Recomendado", "model.group.favorites": "Favoritos", "model.group.collapse": "Recolher {{group}}", @@ -209,6 +210,7 @@ export const dict = { "model.preview.label.average": "Custo Médio Est.", "model.preview.label.context": "Contexto", "model.preview.group.terminalBench": "Terminal Bench 2.0", + "model.preview.group.autoEfficientChoices": "Opções de modelo", "model.preview.label.completion": "Conclusão", "model.preview.label.costAttempt": "Custo / tentativa", "model.preview.value.notSupported": "Não suportado", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts index 1aded053bdb..310add1f5c5 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts @@ -183,6 +183,7 @@ export const dict = { "model.tag.free": "Besplatno", "model.tag.dataCollected": "Podaci se mogu koristiti za obuku", "model.tag.latest": "Najnovije", + "model.group.auto": "Automatski modeli", "model.group.recommended": "Preporučeno", "model.group.favorites": "Favoriti", "model.group.collapse": "Sakrij {{group}}", @@ -210,6 +211,7 @@ export const dict = { "model.preview.label.average": "Procj. prosječni trošak", "model.preview.label.context": "Kontekst", "model.preview.group.terminalBench": "Terminal Bench 2.0", + "model.preview.group.autoEfficientChoices": "Izbori modela", "model.preview.label.completion": "Završetak", "model.preview.label.costAttempt": "Trošak / pokušaj", "model.preview.value.notSupported": "Nije podržano", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/da.ts b/packages/kilo-vscode/webview-ui/src/i18n/da.ts index d47529eec96..5c777244818 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/da.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/da.ts @@ -182,6 +182,7 @@ export const dict = { "model.tag.free": "Gratis", "model.tag.dataCollected": "Data kan bruges til træning", "model.tag.latest": "Nyeste", + "model.group.auto": "Automatiske modeller", "model.group.recommended": "Anbefalet", "model.group.favorites": "Favoritter", "model.group.collapse": "Skjul {{group}}", @@ -210,6 +211,7 @@ export const dict = { "model.preview.label.average": "Est. gennemsnitspris", "model.preview.label.context": "Kontekst", "model.preview.group.terminalBench": "Terminal Bench 2.0", + "model.preview.group.autoEfficientChoices": "Modelvalg", "model.preview.label.completion": "Fuldførelse", "model.preview.label.costAttempt": "Pris / forsøg", "model.preview.value.notSupported": "Ikke understøttet", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/de.ts b/packages/kilo-vscode/webview-ui/src/i18n/de.ts index ceb54153d6f..f4cadb4b058 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/de.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/de.ts @@ -184,6 +184,7 @@ export const dict = { "model.tag.free": "Kostenlos", "model.tag.dataCollected": "Daten können für das Training verwendet werden", "model.tag.latest": "Neueste", + "model.group.auto": "Automatische Modelle", "model.group.recommended": "Empfohlen", "model.group.favorites": "Favoriten", "model.group.collapse": "{{group}} einklappen", @@ -212,6 +213,7 @@ export const dict = { "model.preview.label.average": "Geschätzte Ø-Kosten", "model.preview.label.context": "Kontext", "model.preview.group.terminalBench": "Terminal Bench 2.0", + "model.preview.group.autoEfficientChoices": "Modellauswahl", "model.preview.label.completion": "Abschluss", "model.preview.label.costAttempt": "Kosten / Versuch", "model.preview.value.notSupported": "Nicht unterstützt", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/en.ts b/packages/kilo-vscode/webview-ui/src/i18n/en.ts index df1318c5d97..6742d57bef1 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/en.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/en.ts @@ -228,6 +228,7 @@ export const dict = { "model.tag.free": "Free", "model.tag.dataCollected": "Data may be used for training", "model.tag.latest": "Latest", + "model.group.auto": "Auto Models", "model.group.recommended": "Recommended", "model.group.favorites": "Favorites", "model.group.collapse": "Collapse {{group}}", @@ -255,6 +256,7 @@ export const dict = { "model.preview.label.average": "Est. Average Cost", "model.preview.label.context": "Context", "model.preview.group.terminalBench": "Terminal Bench 2.0", + "model.preview.group.autoEfficientChoices": "Model choices", "model.preview.label.completion": "Completion", "model.preview.label.costAttempt": "Cost / attempt", "model.preview.value.notSupported": "Not supported", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/es.ts b/packages/kilo-vscode/webview-ui/src/i18n/es.ts index dbf2aa87d66..99ae82e6df0 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/es.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/es.ts @@ -183,6 +183,7 @@ export const dict = { "model.tag.free": "Gratis", "model.tag.dataCollected": "Los datos pueden utilizarse para entrenamiento", "model.tag.latest": "Último", + "model.group.auto": "Modelos automáticos", "model.group.recommended": "Recomendado", "model.group.favorites": "Favoritos", "model.group.collapse": "Contraer {{group}}", @@ -211,6 +212,7 @@ export const dict = { "model.preview.label.average": "Costo prom. est.", "model.preview.label.context": "Contexto", "model.preview.group.terminalBench": "Terminal Bench 2.0", + "model.preview.group.autoEfficientChoices": "Opciones de modelo", "model.preview.label.completion": "Finalización", "model.preview.label.costAttempt": "Costo / intento", "model.preview.value.notSupported": "No admitido", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts index 5081a33366e..e39fdefb54d 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts @@ -184,6 +184,7 @@ export const dict = { "model.tag.free": "Gratuit", "model.tag.dataCollected": "Les données peuvent être utilisées pour l’entraînement", "model.tag.latest": "Dernier", + "model.group.auto": "Modèles automatiques", "model.group.recommended": "Recommandé", "model.group.favorites": "Favoris", "model.group.collapse": "Réduire {{group}}", @@ -212,6 +213,7 @@ export const dict = { "model.preview.label.average": "Coût moyen est.", "model.preview.label.context": "Contexte", "model.preview.group.terminalBench": "Terminal Bench 2.0", + "model.preview.group.autoEfficientChoices": "Choix de modèles", "model.preview.label.completion": "Réussite", "model.preview.label.costAttempt": "Coût / tentative", "model.preview.value.notSupported": "Non pris en charge", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/it.ts b/packages/kilo-vscode/webview-ui/src/i18n/it.ts index 5b071a37673..fd3f645aefd 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/it.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/it.ts @@ -158,6 +158,7 @@ export const dict = { "model.tag.free": "Gratis", "model.tag.dataCollected": "I dati possono essere utilizzati per l'addestramento", "model.tag.latest": "Più recente", + "model.group.auto": "Modelli automatici", "model.group.recommended": "Consigliati", "model.group.favorites": "Preferiti", "model.favorite.add": "Aggiungi ai preferiti", @@ -183,6 +184,7 @@ export const dict = { "model.preview.label.average": "Costo medio stimato", "model.preview.label.context": "Contesto", "model.preview.group.terminalBench": "Terminal Bench 2.0", + "model.preview.group.autoEfficientChoices": "Opzioni modello", "model.preview.label.completion": "Completamento", "model.preview.label.costAttempt": "Costo / tentativo", "model.preview.value.notSupported": "Non supportato", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts index f4e3a656750..6cd6423591c 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts @@ -181,6 +181,7 @@ export const dict = { "model.tag.free": "無料", "model.tag.dataCollected": "データがトレーニングに使用される場合があります", "model.tag.latest": "最新", + "model.group.auto": "自動モデル", "model.group.recommended": "推奨", "model.group.favorites": "お気に入り", "model.group.collapse": "{{group}} を折りたたむ", @@ -209,6 +210,7 @@ export const dict = { "model.preview.label.average": "推定平均コスト", "model.preview.label.context": "コンテキスト", "model.preview.group.terminalBench": "Terminal Bench 2.0", + "model.preview.group.autoEfficientChoices": "モデルの選択肢", "model.preview.label.completion": "完了率", "model.preview.label.costAttempt": "試行あたりのコスト", "model.preview.value.notSupported": "未対応", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts index 6b889c3225d..04c2fc699a6 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts @@ -183,6 +183,7 @@ export const dict = { "model.tag.free": "무료", "model.tag.dataCollected": "데이터가 학습에 사용될 수 있습니다", "model.tag.latest": "최신", + "model.group.auto": "자동 모델", "model.group.recommended": "추천", "model.group.favorites": "즐겨찾기", "model.group.collapse": "{{group}} 접기", @@ -211,6 +212,7 @@ export const dict = { "model.preview.label.average": "예상 평균 비용", "model.preview.label.context": "컨텍스트", "model.preview.group.terminalBench": "Terminal Bench 2.0", + "model.preview.group.autoEfficientChoices": "모델 선택지", "model.preview.label.completion": "완료율", "model.preview.label.costAttempt": "시도당 비용", "model.preview.value.notSupported": "미지원", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts index b573c3ae87f..15e07ecddd8 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts @@ -183,6 +183,7 @@ export const dict = { "model.tag.free": "Gratis", "model.tag.dataCollected": "Gegevens kunnen worden gebruikt voor training", "model.tag.latest": "Nieuwste", + "model.group.auto": "Automatische modellen", "model.group.recommended": "Aanbevolen", "model.group.favorites": "Favorieten", "model.group.collapse": "{{group}} inklappen", @@ -210,6 +211,7 @@ export const dict = { "model.preview.label.average": "Gesch. gem. kosten", "model.preview.label.context": "Context", "model.preview.group.terminalBench": "Terminal Bench 2.0", + "model.preview.group.autoEfficientChoices": "Modelkeuzes", "model.preview.label.completion": "Voltooiing", "model.preview.label.costAttempt": "Kosten / poging", "model.preview.value.notSupported": "Niet ondersteund", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/no.ts b/packages/kilo-vscode/webview-ui/src/i18n/no.ts index 0bfe7f19828..2e1f8aee272 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/no.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/no.ts @@ -183,6 +183,7 @@ export const dict = { "model.tag.free": "Gratis", "model.tag.dataCollected": "Data kan brukes til trening", "model.tag.latest": "Nyeste", + "model.group.auto": "Automatiske modeller", "model.group.recommended": "Anbefalt", "model.group.favorites": "Favoritter", "model.group.collapse": "Skjul {{group}}", @@ -210,6 +211,7 @@ export const dict = { "model.preview.label.average": "Est. snittkostnad", "model.preview.label.context": "Kontekst", "model.preview.group.terminalBench": "Terminal Bench 2.0", + "model.preview.group.autoEfficientChoices": "Modellvalg", "model.preview.label.completion": "Fullføring", "model.preview.label.costAttempt": "Kostnad / forsøk", "model.preview.value.notSupported": "Ikke støttet", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts index d32bb04b548..921c0b5793a 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts @@ -182,6 +182,7 @@ export const dict = { "model.tag.free": "Darmowy", "model.tag.dataCollected": "Dane mogą być wykorzystywane do trenowania", "model.tag.latest": "Najnowszy", + "model.group.auto": "Modele automatyczne", "model.group.recommended": "Zalecane", "model.group.favorites": "Ulubione", "model.group.collapse": "Zwiń {{group}}", @@ -209,6 +210,7 @@ export const dict = { "model.preview.label.average": "Szac. średni koszt", "model.preview.label.context": "Kontekst", "model.preview.group.terminalBench": "Terminal Bench 2.0", + "model.preview.group.autoEfficientChoices": "Wybór modeli", "model.preview.label.completion": "Ukończenie", "model.preview.label.costAttempt": "Koszt / próbę", "model.preview.value.notSupported": "Nieobsługiwane", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts index b3eaede058d..1fb900ca781 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts @@ -182,6 +182,7 @@ export const dict = { "model.tag.free": "Бесплатно", "model.tag.dataCollected": "Данные могут использоваться для обучения", "model.tag.latest": "Последняя", + "model.group.auto": "Автоматические модели", "model.group.recommended": "Рекомендуемые", "model.group.favorites": "Избранное", "model.group.collapse": "Свернуть {{group}}", @@ -209,6 +210,7 @@ export const dict = { "model.preview.label.average": "Средняя стоимость", "model.preview.label.context": "Контекст", "model.preview.group.terminalBench": "Terminal Bench 2.0", + "model.preview.group.autoEfficientChoices": "Варианты моделей", "model.preview.label.completion": "Выполнение", "model.preview.label.costAttempt": "Стоимость / попытка", "model.preview.value.notSupported": "Не поддерживается", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/th.ts b/packages/kilo-vscode/webview-ui/src/i18n/th.ts index d08ecd6fb06..87a88c2a7e6 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/th.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/th.ts @@ -182,6 +182,7 @@ export const dict = { "model.tag.free": "ฟรี", "model.tag.dataCollected": "ข้อมูลอาจถูกนำไปใช้ในการฝึก", "model.tag.latest": "ล่าสุด", + "model.group.auto": "โมเดลอัตโนมัติ", "model.group.recommended": "แนะนำ", "model.group.favorites": "รายการโปรด", "model.group.collapse": "ย่อ {{group}}", @@ -209,6 +210,7 @@ export const dict = { "model.preview.label.average": "ต้นทุนเฉลี่ยโดยประมาณ", "model.preview.label.context": "บริบท", "model.preview.group.terminalBench": "Terminal Bench 2.0", + "model.preview.group.autoEfficientChoices": "ตัวเลือกโมเดล", "model.preview.label.completion": "ความสำเร็จ", "model.preview.label.costAttempt": "ค่าใช้จ่าย / ครั้ง", "model.preview.value.notSupported": "ไม่รองรับ", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts index fd11169004a..6999fa62ee0 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts @@ -182,6 +182,7 @@ export const dict = { "model.tag.free": "Ücretsiz", "model.tag.dataCollected": "Veriler eğitim için kullanılabilir", "model.tag.latest": "En yeni", + "model.group.auto": "Otomatik Modeller", "model.group.recommended": "Önerilen", "model.group.favorites": "Favoriler", "model.group.collapse": "{{group}} grubunu daralt", @@ -209,6 +210,7 @@ export const dict = { "model.preview.label.average": "Tahmini Ort. Maliyet", "model.preview.label.context": "Bağlam", "model.preview.group.terminalBench": "Terminal Bench 2.0", + "model.preview.group.autoEfficientChoices": "Model seçenekleri", "model.preview.label.completion": "Tamamlama", "model.preview.label.costAttempt": "Maliyet / deneme", "model.preview.value.notSupported": "Desteklenmiyor", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/uk.ts b/packages/kilo-vscode/webview-ui/src/i18n/uk.ts index bbcf4fd861d..decb26cabb5 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/uk.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/uk.ts @@ -183,6 +183,7 @@ export const dict = { "model.tag.free": "Безкоштовно", "model.tag.dataCollected": "Дані можуть використовуватися для навчання", "model.tag.latest": "Остання", + "model.group.auto": "Автоматичні моделі", "model.group.recommended": "Рекомендовані", "model.group.favorites": "Обране", "model.group.collapse": "Згорнути {{group}}", @@ -210,6 +211,7 @@ export const dict = { "model.preview.label.average": "Орієнт. сер. вартість", "model.preview.label.context": "Контекст", "model.preview.group.terminalBench": "Terminal Bench 2.0", + "model.preview.group.autoEfficientChoices": "Варіанти моделей", "model.preview.label.completion": "Виконання", "model.preview.label.costAttempt": "Вартість / спроба", "model.preview.value.notSupported": "Не підтримується", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts index a467d191df0..05aff59870a 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts @@ -180,6 +180,7 @@ export const dict = { "model.tag.free": "免费", "model.tag.dataCollected": "数据可能会用于训练", "model.tag.latest": "最新", + "model.group.auto": "自动模型", "model.group.recommended": "推荐", "model.group.favorites": "收藏夹", "model.group.collapse": "折叠 {{group}}", @@ -208,6 +209,7 @@ export const dict = { "model.preview.label.average": "预估平均成本", "model.preview.label.context": "上下文", "model.preview.group.terminalBench": "Terminal Bench 2.0", + "model.preview.group.autoEfficientChoices": "模型选择", "model.preview.label.completion": "完成率", "model.preview.label.costAttempt": "成本 / 次尝试", "model.preview.value.notSupported": "不支持", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts index fe6ad65556c..f5744b078dd 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts @@ -180,6 +180,7 @@ export const dict = { "model.tag.free": "免費", "model.tag.dataCollected": "資料可能會用於訓練", "model.tag.latest": "最新", + "model.group.auto": "自動模型", "model.group.recommended": "推薦", "model.group.favorites": "我的最愛", "model.group.collapse": "折疊 {{group}}", @@ -208,6 +209,7 @@ export const dict = { "model.preview.label.average": "預估平均成本", "model.preview.label.context": "上下文", "model.preview.group.terminalBench": "Terminal Bench 2.0", + "model.preview.group.autoEfficientChoices": "模型選擇", "model.preview.label.completion": "完成率", "model.preview.label.costAttempt": "成本 / 次嘗試", "model.preview.value.notSupported": "不支援", diff --git a/packages/kilo-vscode/webview-ui/src/stories/shared.stories.tsx b/packages/kilo-vscode/webview-ui/src/stories/shared.stories.tsx index 2efefc401b2..ca1c3abe408 100644 --- a/packages/kilo-vscode/webview-ui/src/stories/shared.stories.tsx +++ b/packages/kilo-vscode/webview-ui/src/stories/shared.stories.tsx @@ -38,6 +38,19 @@ export const ModelSelectorNoProviders: Story = { } const ACCESSIBLE_MODELS: EnrichedModel[] = [ + { + id: "kilo-auto/efficient", + name: "Kilo Auto Efficient", + providerID: "kilo", + providerName: "Kilo", + recommendedIndex: 0, + options: { + description: + "Routes each request to the cheapest model that gets the job done, based on continuously benchmarked accuracy and cost.", + }, + autoRouting: { models: ["google/gemini-2.5-flash", "anthropic/claude-sonnet-4.6"] }, + }, + { id: "omega", name: "Omega", providerID: "openai", providerName: "OpenAI", recommendedIndex: 1 }, { id: "alpha", name: "Alpha", providerID: "kilo", providerName: "Kilo" }, { id: "bravo", name: "Bravo", providerID: "kilo", providerName: "Kilo" }, { id: "charlie", name: "Charlie", providerID: "kilo", providerName: "Kilo" }, diff --git a/packages/kilo-vscode/webview-ui/src/styles/model-selector.css b/packages/kilo-vscode/webview-ui/src/styles/model-selector.css index 387d1bd395e..e16816425e6 100644 --- a/packages/kilo-vscode/webview-ui/src/styles/model-selector.css +++ b/packages/kilo-vscode/webview-ui/src/styles/model-selector.css @@ -17,12 +17,14 @@ } .model-selector-body .model-selector-list { + order: 3; flex: 1; min-height: 0; max-height: none; } .model-selector-preview { + order: 1; flex-shrink: 0; overflow: hidden; height: 0; @@ -154,6 +156,40 @@ color: var(--vscode-editor-foreground); } +.model-preview-candidates { + display: flex; + flex-direction: column; + gap: 4px; +} + +.model-preview-candidate { + display: flex; + flex-direction: column; + gap: 1px; + min-width: 0; + padding: 3px 5px; + border-radius: 3px; + background: var(--vscode-textCodeBlock-background); + color: var(--vscode-editor-foreground); +} + +.model-preview-candidate-name, +.model-preview-candidate-id { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.model-preview-candidate-name { + font-size: var(--kilo-font-size-11); +} + +.model-preview-candidate-id { + font-family: var(--vscode-editor-font-family, monospace); + font-size: var(--kilo-font-size-10); + color: var(--vscode-descriptionForeground); +} + .model-preview-data-line { grid-column: 1 / -1; display: inline-flex; @@ -202,6 +238,8 @@ } .model-preview-free-data-icon, +.model-preview-auto-icon, +.model-selector-auto-icon, .model-selector-free-data-icon { display: inline-flex; align-items: center; @@ -209,12 +247,20 @@ flex-shrink: 0; } +.model-preview-auto-icon, +.model-selector-auto-icon { + --icon-base: var(--vscode-list-warningForeground, var(--vscode-inputValidation-warningForeground, #9a6700)); + color: var(--icon-base); + opacity: 0.95; +} + .model-selector-preview--visible { /* height is set inline via previewHeight signal */ border-top-width: 0; } .model-selector-splitter { + order: 2; flex-shrink: 0; height: 5px; cursor: row-resize; @@ -262,6 +308,7 @@ } .model-selector-search-wrapper { + order: 0; display: flex; align-items: center; gap: 4px; @@ -428,6 +475,12 @@ text-overflow: ellipsis; } +.model-selector-item.active .model-selector-auto-icon { + --icon-base: var(--vscode-list-warningForeground, var(--vscode-inputValidation-warningForeground, #9a6700)); + color: var(--icon-base); + opacity: 1; +} + .model-selector-data-badge [data-component="tag"] { flex-shrink: 0; background: var(--vscode-testing-iconPassed, #4caf50) !important; diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/providers.ts b/packages/kilo-vscode/webview-ui/src/types/messages/providers.ts index 9726f263f36..cb9b1094470 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/providers.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/providers.ts @@ -16,6 +16,7 @@ export interface ProviderModel { input?: { text: boolean; image: boolean; audio: boolean; video: boolean; pdf: boolean } } options?: { description?: string } + autoRouting?: { models: string[] } recommendedIndex?: number isFree?: boolean mayTrainOnYourPrompts?: boolean diff --git a/packages/opencode/src/kilocode/provider/provider.ts b/packages/opencode/src/kilocode/provider/provider.ts index efcf0a0f6e8..66e47a8fa59 100644 --- a/packages/opencode/src/kilocode/provider/provider.ts +++ b/packages/opencode/src/kilocode/provider/provider.ts @@ -43,6 +43,11 @@ export const KILO_MODEL_SCHEMA_EXTENSIONS = { avgAttemptCostUsd: Schema.Finite, }), ), + autoRouting: optionalOmitUndefined( + Schema.Struct({ + models: Schema.Array(Schema.String), + }), + ), ai_sdk_provider: Schema.optional(Schema.Literals(AI_SDK_PROVIDERS)), } @@ -59,6 +64,7 @@ export function patchModelsDevModel(providerID: string, source: any) { mayTrainOnYourPrompts: source.mayTrainOnYourPrompts, hasUserByokAvailable: source.hasUserByokAvailable, terminalBench: source.terminalBench, + autoRouting: source.autoRouting, ai_sdk_provider: source.ai_sdk_provider, options: source.options ?? {}, } @@ -76,6 +82,7 @@ export function patchConfigModel(cfg: any, existing: any) { mayTrainOnYourPrompts: cfg.mayTrainOnYourPrompts ?? existing?.mayTrainOnYourPrompts, hasUserByokAvailable: cfg.hasUserByokAvailable ?? existing?.hasUserByokAvailable, terminalBench: existing?.terminalBench, + autoRouting: existing?.autoRouting, ai_sdk_provider: cfg.ai_sdk_provider ?? existing?.ai_sdk_provider, variants: cfg.variants ? mapValues( diff --git a/packages/opencode/test/kilocode/provider-model-metadata.test.ts b/packages/opencode/test/kilocode/provider-model-metadata.test.ts new file mode 100644 index 00000000000..afdafdf3820 --- /dev/null +++ b/packages/opencode/test/kilocode/provider-model-metadata.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, test } from "bun:test" +import { Schema } from "effect" +import { Provider } from "../../src/provider/provider" +import { patchModelsDevModel } from "../../src/kilocode/provider/provider" + +describe("Kilo provider model metadata", () => { + test("preserves Auto Efficient routing models from Models.dev data", () => { + const patch = patchModelsDevModel("kilo", { + autoRouting: { models: ["google/gemini-2.5-flash", "anthropic/claude-sonnet-4.6"] }, + }) + + expect(patch.autoRouting).toEqual({ + models: ["google/gemini-2.5-flash", "anthropic/claude-sonnet-4.6"], + }) + }) + + test("Provider.Model schema accepts Auto Efficient routing models", () => { + const model = Schema.decodeUnknownSync(Provider.Model)({ + id: "kilo-auto/efficient", + providerID: "kilo", + api: { id: "kilo", url: "https://kilocode.ai", npm: "@kilocode/kilo-gateway" }, + name: "Kilo Auto Efficient", + family: "kilo-auto", + capabilities: { + temperature: true, + reasoning: false, + attachment: false, + toolcall: true, + input: { text: true, audio: false, image: false, video: false, pdf: false }, + output: { text: true, audio: false, image: false, video: false, pdf: false }, + interleaved: false, + }, + cost: { input: 0, output: 0, cache: { read: 0, write: 0 } }, + limit: { context: 128000, output: 16384 }, + status: "active", + options: {}, + headers: {}, + release_date: "2026-06-26", + variants: {}, + autoRouting: { models: ["google/gemini-2.5-flash"] }, + }) + + expect(model.autoRouting).toEqual({ models: ["google/gemini-2.5-flash"] }) + }) +}) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 45a6177cf6b..a4a24d6cdfb 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1712,6 +1712,9 @@ export type Model = { overallScore: number avgAttemptCostUsd: number } + autoRouting?: { + models: Array + } ai_sdk_provider?: "alibaba" | "anthropic" | "mistral" | "openai" | "openai-compatible" | "openrouter" } diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 5dd94a56b47..7aefcd47f03 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -22372,6 +22372,19 @@ "required": ["overallScore", "avgAttemptCostUsd"], "additionalProperties": false }, + "autoRouting": { + "type": "object", + "properties": { + "models": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": ["models"], + "additionalProperties": false + }, "ai_sdk_provider": { "type": "string", "enum": ["alibaba", "anthropic", "mistral", "openai", "openai-compatible", "openrouter"]