mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-31 01:37:28 +08:00
Merge pull request #12943 from Kilo-Org/improve-model-search-ranking
feat(vscode): improve model search ranking
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": minor
|
||||
---
|
||||
|
||||
Improve model search relevance with provider-aware results and personalized usage suggestions.
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:89fa6a619fc2089fd0bb03dabe191394d3dcf3423ba066f1e8f2e2c6aac38838
|
||||
size 1085
|
||||
@@ -89,6 +89,7 @@ import {
|
||||
} from "./services/autocomplete/settings"
|
||||
import { routeEarlyMessage } from "./kilo-provider/early-message"
|
||||
import * as ModelState from "./kilo-provider/model-state"
|
||||
import { handleModelUsageMessage } from "./kilo-provider/model-usage"
|
||||
import { handleForkSession } from "./kilo-provider/fork-session"
|
||||
import { openConfig } from "./kilo-provider/open-config"
|
||||
import {
|
||||
@@ -1025,6 +1026,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
copy: (text) => vscode.env.clipboard.writeText(text),
|
||||
openSessions: (ids) => this.trackOpenSessions(ids),
|
||||
speechToTextModels: () => this.fetchAndSendSpeechToTextModels(),
|
||||
modelUsage: (msg) => handleModelUsageMessage(msg, this.extensionContext, (value) => this.postMessage(value)),
|
||||
})
|
||||
) {
|
||||
return
|
||||
@@ -4057,6 +4059,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
// Clear globalState items that are not part of the configuration
|
||||
await this.extensionContext?.globalState.update("variantSelections", undefined)
|
||||
await this.extensionContext?.globalState.update("recentModels", undefined)
|
||||
await this.extensionContext?.globalState.update("modelUsage", undefined)
|
||||
await this.extensionContext?.globalState.update("kilo.dismissedNotificationIds", undefined)
|
||||
await this.extensionContext?.globalState.update("kilo.agentMigrationBannerDismissed", undefined)
|
||||
await this.extensionContext?.globalState.update("kilo.marketplace.dismissedSuggestions", undefined)
|
||||
@@ -4074,6 +4077,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
// Re-send globalState items to the webview
|
||||
this.postMessage({ type: "variantsLoaded", variants: {} })
|
||||
this.postMessage({ type: "recentsLoaded", recents: [] })
|
||||
this.postMessage({ type: "modelUsageLoaded", usage: {} })
|
||||
|
||||
// Re-fetch notifications to reflect cleared dismissed IDs
|
||||
await this.fetchAndSendNotifications()
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { SuggestionContext } from "./handlers/suggestion"
|
||||
import type { KiloClient } from "@kilocode/sdk/v2/client"
|
||||
import { buildChatSettingsMessage } from "./chat-settings"
|
||||
import { buildThroughputSettingMessage } from "./throughput-settings"
|
||||
import { handleModelUsageMessage, type ModelUsageMessage } from "./model-usage"
|
||||
|
||||
type Ctx = {
|
||||
question: SuggestionContext
|
||||
@@ -18,6 +19,7 @@ type Ctx = {
|
||||
copy: (text: string) => PromiseLike<void>
|
||||
openSessions: (ids: string[]) => void
|
||||
speechToTextModels: () => Promise<void>
|
||||
modelUsage: (message: ModelUsageMessage) => Promise<void>
|
||||
}
|
||||
|
||||
export async function routeEarlyMessage(
|
||||
@@ -42,6 +44,10 @@ export async function routeEarlyMessage(
|
||||
)
|
||||
return true
|
||||
}
|
||||
if (message.type === "recordModelUsage" || message.type === "requestModelUsage") {
|
||||
await ctx.modelUsage(message as ModelUsageMessage)
|
||||
return true
|
||||
}
|
||||
await routeSuggestionWebviewMessage(ctx.question, message)
|
||||
if (await ModelState.handleMessage(message.type, message, ctx.client, ctx.post)) return true
|
||||
if (message.type === "exportSessionTranscript") {
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
const LIMIT = 200
|
||||
export type ModelUsageMap = Record<string, { count: number; lastUsed: number }>
|
||||
export type ModelUsageMessage =
|
||||
| { type: "recordModelUsage"; providerID: string; modelID: string }
|
||||
| { type: "requestModelUsage" }
|
||||
|
||||
function valid(value: unknown): value is { count: number; lastUsed: number } {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) return false
|
||||
const item = value as Record<string, unknown>
|
||||
const count = item.count
|
||||
const lastUsed = item.lastUsed
|
||||
return (
|
||||
typeof count === "number" &&
|
||||
Number.isFinite(count) &&
|
||||
count > 0 &&
|
||||
typeof lastUsed === "number" &&
|
||||
Number.isFinite(lastUsed)
|
||||
)
|
||||
}
|
||||
|
||||
export function validateModelUsage(raw: unknown): ModelUsageMap {
|
||||
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {}
|
||||
const entries = Object.entries(raw as Record<string, unknown>).flatMap(([key, value]) =>
|
||||
valid(value) ? [[key, value] as const] : [],
|
||||
)
|
||||
return Object.fromEntries(
|
||||
entries
|
||||
.sort(([, a], [, b]) => b.lastUsed - a.lastUsed)
|
||||
.slice(0, LIMIT)
|
||||
.map(([key, value]) => [
|
||||
key,
|
||||
{
|
||||
count: Math.floor(value.count),
|
||||
lastUsed: value.lastUsed,
|
||||
},
|
||||
]),
|
||||
)
|
||||
}
|
||||
|
||||
export function recordModelUsage(raw: unknown, providerID: unknown, modelID: unknown, now = Date.now()): ModelUsageMap {
|
||||
if (typeof providerID !== "string" || !providerID || typeof modelID !== "string" || !modelID) {
|
||||
return validateModelUsage(raw)
|
||||
}
|
||||
const usage = validateModelUsage(raw)
|
||||
const key = `${providerID}/${modelID}`
|
||||
const current = usage[key] ?? { count: 0, lastUsed: 0 }
|
||||
usage[key] = { count: current.count + 1, lastUsed: now }
|
||||
return validateModelUsage(usage)
|
||||
}
|
||||
|
||||
export async function handleModelUsageMessage(
|
||||
message: ModelUsageMessage,
|
||||
context:
|
||||
| { globalState: { get: (key: string) => unknown; update: (key: string, value: unknown) => Thenable<void> } }
|
||||
| undefined,
|
||||
post: (message: unknown) => void,
|
||||
): Promise<void> {
|
||||
const current = context?.globalState.get("modelUsage")
|
||||
const usage =
|
||||
message.type === "recordModelUsage"
|
||||
? recordModelUsage(current, message.providerID, message.modelID)
|
||||
: validateModelUsage(current)
|
||||
if (message.type === "recordModelUsage") await context?.globalState.update("modelUsage", usage)
|
||||
post({ type: "modelUsageLoaded", usage })
|
||||
}
|
||||
@@ -80,17 +80,18 @@ test("auto efficient details show server description and model choices", async (
|
||||
await expect(preview).not.toContainText("openai/gpt-5.5")
|
||||
})
|
||||
|
||||
test("typing a provider initial moves the active descendant to matching results", async ({ page }) => {
|
||||
test("search uses a flat relevance-ranked result list with provider labels", async ({ page }) => {
|
||||
await load(page, "shared--model-selector-accessible")
|
||||
|
||||
await page.getByRole("button", { name: "Review model: Alpha" }).click()
|
||||
const combobox = page.getByRole("combobox", { name: "Review model: Alpha. Search models" })
|
||||
await combobox.fill("N")
|
||||
await combobox.fill("nov")
|
||||
|
||||
const nova = page.getByRole("treeitem", { name: "Nova" })
|
||||
await expect(nova).toBeVisible()
|
||||
await expect(combobox).toHaveAttribute("aria-activedescendant", await nova.getAttribute("id"))
|
||||
await expect(page.getByRole("treeitem", { name: "NVIDIA" })).toHaveAttribute("aria-expanded", "true")
|
||||
await expect(page.locator(".model-selector-group-label").filter({ hasText: "NVIDIA" })).toHaveCount(0)
|
||||
await expect(nova).toContainText("NVIDIA")
|
||||
})
|
||||
|
||||
test("provider groups collapse, expand, and skip their model rows", async ({ page }) => {
|
||||
@@ -141,12 +142,13 @@ test("active descendant always identifies a visible tree item", async ({ page })
|
||||
await active()
|
||||
await combobox.fill("N")
|
||||
await active()
|
||||
await combobox.press("ArrowLeft")
|
||||
await combobox.press("ArrowDown")
|
||||
await combobox.press("ArrowLeft")
|
||||
await active()
|
||||
await combobox.fill("no matching model")
|
||||
await active()
|
||||
await expect(combobox).toHaveAttribute(
|
||||
"aria-activedescendant",
|
||||
await page.getByRole("treeitem", { name: "Use default model" }).getAttribute("id"),
|
||||
)
|
||||
})
|
||||
|
||||
test("expanded preview waits for explicit pointer selection", async ({ page }) => {
|
||||
|
||||
@@ -13,7 +13,10 @@ import {
|
||||
isAuto,
|
||||
autoSummary,
|
||||
autoChoices,
|
||||
rankModelSearch,
|
||||
mostUsedModels,
|
||||
} from "../../webview-ui/src/components/shared/model-selector-utils"
|
||||
import type { EnrichedModel } from "../../webview-ui/src/context/provider"
|
||||
|
||||
const labels = { select: "Select model", noProviders: "No providers", notSet: "Not set" }
|
||||
|
||||
@@ -169,6 +172,51 @@ describe("autoSummary", () => {
|
||||
})
|
||||
})
|
||||
|
||||
const SEARCH_MODELS: EnrichedModel[] = [
|
||||
{ id: "solar-pro", name: "Solar Pro", providerID: "nvidia", providerName: "NVIDIA" },
|
||||
{ id: "gpt-5.6-sol", name: "GPT-5.6 Sol", providerID: "openai", providerName: "OpenAI" },
|
||||
{ id: "gpt-5.6-sol", name: "GPT-5.6 Sol", providerID: "kilo", providerName: "Kilo" },
|
||||
{ id: "gpt-5.6", name: "GPT-5.6", providerID: "anthropic", providerName: "Anthropic" },
|
||||
]
|
||||
|
||||
describe("rankModelSearch", () => {
|
||||
it("prefers an exact model token over a longer prefix match", () => {
|
||||
expect(
|
||||
rankModelSearch(SEARCH_MODELS, "sol")
|
||||
.slice(0, 2)
|
||||
.map((model) => model.name),
|
||||
).toEqual(["GPT-5.6 Sol", "GPT-5.6 Sol"])
|
||||
})
|
||||
|
||||
it("keeps provider variants together and uses usage to order equivalent variants", () => {
|
||||
const result = rankModelSearch(SEARCH_MODELS, "sol", {
|
||||
usage: { "kilo/gpt-5.6-sol": { count: 4, lastUsed: 10 }, "openai/gpt-5.6-sol": { count: 1, lastUsed: 20 } },
|
||||
})
|
||||
expect(result.slice(0, 2).map((model) => model.providerID)).toEqual(["kilo", "openai"])
|
||||
})
|
||||
|
||||
it("does not let usage make a weaker model beat an exact match", () => {
|
||||
const result = rankModelSearch(SEARCH_MODELS, "sol", {
|
||||
usage: { "nvidia/solar-pro": { count: 1000, lastUsed: 100 } },
|
||||
})
|
||||
expect(result[0]?.name).toBe("GPT-5.6 Sol")
|
||||
})
|
||||
})
|
||||
|
||||
describe("mostUsedModels", () => {
|
||||
it("orders suggestions by personal count and excludes favorites", () => {
|
||||
const result = mostUsedModels(
|
||||
SEARCH_MODELS,
|
||||
{
|
||||
"nvidia/solar-pro": { count: 2, lastUsed: 20 },
|
||||
"openai/gpt-5.6-sol": { count: 5, lastUsed: 10 },
|
||||
},
|
||||
new Set(["openai/gpt-5.6-sol"]),
|
||||
)
|
||||
expect(result.map((model) => model.providerID)).toEqual(["nvidia"])
|
||||
})
|
||||
})
|
||||
|
||||
describe("isDataCollectedModel", () => {
|
||||
it("uses only explicit prompt training metadata", () => {
|
||||
expect(isDataCollectedModel({ mayTrainOnYourPrompts: true })).toBe(true)
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { describe, expect, it } from "bun:test"
|
||||
import { recordModelUsage, validateModelUsage } from "../../src/kilo-provider/model-usage"
|
||||
|
||||
describe("model usage history", () => {
|
||||
it("increments a model and updates its last-used timestamp", () => {
|
||||
expect(recordModelUsage({ "openai/gpt": { count: 2, lastUsed: 10 } }, "openai", "gpt", 20)).toEqual({
|
||||
"openai/gpt": { count: 3, lastUsed: 20 },
|
||||
})
|
||||
})
|
||||
|
||||
it("drops malformed entries and caps persisted history", () => {
|
||||
const raw = Object.fromEntries(
|
||||
Array.from({ length: 205 }, (_, index) => [`provider/model-${index}`, { count: 1, lastUsed: index }]),
|
||||
)
|
||||
const result = validateModelUsage({ ...raw, invalid: { count: 0, lastUsed: 1 } })
|
||||
expect(Object.keys(result)).toHaveLength(200)
|
||||
expect(result["provider/model-204"]).toEqual({ count: 1, lastUsed: 204 })
|
||||
expect(result.invalid).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -45,9 +45,10 @@ import {
|
||||
autoSummary,
|
||||
buildTriggerLabel,
|
||||
sanitizeName,
|
||||
mostUsedModels,
|
||||
rankModelSearch,
|
||||
} from "./model-selector-utils"
|
||||
import { ModelPreview } from "./ModelPreview"
|
||||
import { searchMatch } from "../../utils/search-match"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Row / group key helpers — single source of truth for key formatting
|
||||
@@ -57,6 +58,7 @@ const CLEAR_KEY = "clear"
|
||||
const FAVORITES_KEY = "favorites"
|
||||
const AUTO_KEY = "auto"
|
||||
const RECOMMENDED_KEY = "recommended"
|
||||
const MOST_USED_KEY = "most-used"
|
||||
|
||||
function modelKey(providerID: string, modelID: string) {
|
||||
return `${providerID}/${modelID}`
|
||||
@@ -82,7 +84,7 @@ interface ModelRow {
|
||||
|
||||
interface ModelGroup {
|
||||
key: string
|
||||
label: string
|
||||
label?: string
|
||||
rows: ModelRow[]
|
||||
}
|
||||
|
||||
@@ -161,6 +163,7 @@ export const ModelSelectorBase: Component<ModelSelectorBaseProps> = (props) => {
|
||||
const expanded = vscode.getModelSelectorExpanded
|
||||
const setExpanded = vscode.setModelSelectorExpanded
|
||||
const [search, setSearch] = createSignal("")
|
||||
const hasSearch = () => search().trim().length > 0
|
||||
const [selectedKey, setSelectedKey] = createSignal(CLEAR_KEY)
|
||||
const [browsing, setBrowsing] = createSignal(false)
|
||||
const [navigating, setNavigating] = createSignal(false)
|
||||
@@ -234,9 +237,11 @@ export const ModelSelectorBase: Component<ModelSelectorBaseProps> = (props) => {
|
||||
if (!q) {
|
||||
return visibleModels()
|
||||
}
|
||||
return visibleModels().filter(
|
||||
(m) => searchMatch(q, m.name) || searchMatch(q, m.id) || searchMatch(q, m.providerName),
|
||||
)
|
||||
return rankModelSearch(visibleModels(), q, {
|
||||
usage: session?.modelUsageHistory(),
|
||||
favorites: new Set(session?.favoriteModels().map((item) => modelKey(item.providerID, item.modelID))),
|
||||
recent: session?.recentModels(),
|
||||
})
|
||||
})
|
||||
|
||||
// Live set of favorited keys — drives star icon visual state (filled vs outline).
|
||||
@@ -249,7 +254,7 @@ export const ModelSelectorBase: Component<ModelSelectorBaseProps> = (props) => {
|
||||
|
||||
const favoriteModels = createMemo(() => {
|
||||
if (props.favorites === false) return []
|
||||
if (!session || search()) return []
|
||||
if (!session || hasSearch()) return []
|
||||
const map = new Map(visibleModels().map((m) => [modelKey(m.providerID, m.id), m]))
|
||||
const list = session
|
||||
.favoriteModels()
|
||||
@@ -267,13 +272,30 @@ export const ModelSelectorBase: Component<ModelSelectorBaseProps> = (props) => {
|
||||
const groups = createMemo<ModelGroup[]>(() => {
|
||||
const autos: EnrichedModel[] = []
|
||||
const recommended: EnrichedModel[] = []
|
||||
const mostUsed: EnrichedModel[] = []
|
||||
const map = new Map<string, EnrichedModel[]>()
|
||||
|
||||
if (!hasSearch() && session) {
|
||||
mostUsed.push(
|
||||
...mostUsedModels(
|
||||
visibleModels().filter((model) => !isAuto(model) && model.recommendedIndex === undefined),
|
||||
session.modelUsageHistory(),
|
||||
favoriteKeys(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
for (const m of filtered()) {
|
||||
if (isAuto(m)) {
|
||||
autos.push(m)
|
||||
continue
|
||||
}
|
||||
if (
|
||||
!hasSearch() &&
|
||||
mostUsed.some((item) => modelKey(item.providerID, item.id) === modelKey(m.providerID, m.id))
|
||||
) {
|
||||
continue
|
||||
}
|
||||
if (m.recommendedIndex !== undefined) {
|
||||
recommended.push(m)
|
||||
continue
|
||||
@@ -328,6 +350,18 @@ export const ModelSelectorBase: Component<ModelSelectorBaseProps> = (props) => {
|
||||
})
|
||||
}
|
||||
|
||||
if (mostUsed.length > 0) {
|
||||
result.push({
|
||||
key: MOST_USED_KEY,
|
||||
label: language.t("model.group.mostUsed"),
|
||||
rows: mostUsed.map((m) => ({
|
||||
key: rowKey("model", m.providerID, m.id),
|
||||
kind: "model",
|
||||
model: m,
|
||||
})),
|
||||
})
|
||||
}
|
||||
|
||||
const rest: ModelGroup[] = [...map.entries()]
|
||||
.sort(([a], [b]) => providerSortKey(a) - providerSortKey(b))
|
||||
.map(([id, list]) => {
|
||||
@@ -343,11 +377,24 @@ export const ModelSelectorBase: Component<ModelSelectorBaseProps> = (props) => {
|
||||
}
|
||||
})
|
||||
|
||||
if (hasSearch()) {
|
||||
if (filtered().length === 0) return []
|
||||
return [
|
||||
{
|
||||
key: "search-results",
|
||||
rows: filtered().map((m) => ({
|
||||
key: rowKey("model", m.providerID, m.id),
|
||||
kind: "model",
|
||||
model: m,
|
||||
})),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
return [...result, ...rest]
|
||||
})
|
||||
|
||||
// Collapse state is honored even during search so users can skip past
|
||||
// large providers (e.g. Kilo Gateway) without scrolling through every match.
|
||||
// Search results are flattened so matching provider variants stay adjacent.
|
||||
const isGroupOpen = (key: string) => !collapsed().has(key)
|
||||
|
||||
function toggleGroup(key: string) {
|
||||
@@ -366,7 +413,7 @@ export const ModelSelectorBase: Component<ModelSelectorBaseProps> = (props) => {
|
||||
|
||||
const rows = createMemo<ModelRow[]>(() => {
|
||||
const c = collapsed()
|
||||
const list = groups().flatMap((g) => (c.has(g.key) ? [] : g.rows))
|
||||
const list = groups().flatMap((g) => (hasSearch() || !c.has(g.key) ? g.rows : []))
|
||||
if (!props.allowClear) return list
|
||||
return [{ key: CLEAR_KEY, kind: "clear" }, ...list]
|
||||
})
|
||||
@@ -375,6 +422,10 @@ export const ModelSelectorBase: Component<ModelSelectorBaseProps> = (props) => {
|
||||
const result: ModelNode[] = []
|
||||
if (props.allowClear) result.push({ key: CLEAR_KEY, kind: "row", row: { key: CLEAR_KEY, kind: "clear" } })
|
||||
for (const group of groups()) {
|
||||
if (hasSearch()) {
|
||||
result.push(...group.rows.map((row) => ({ key: row.key, kind: "row" as const, row, group })))
|
||||
continue
|
||||
}
|
||||
result.push({ key: groupKey(group.key), kind: "group", group })
|
||||
if (!isGroupOpen(group.key)) continue
|
||||
result.push(...group.rows.map((row) => ({ key: row.key, kind: "row" as const, row, group })))
|
||||
@@ -398,7 +449,7 @@ export const ModelSelectorBase: Component<ModelSelectorBaseProps> = (props) => {
|
||||
if (!m) return props.allowClear ? CLEAR_KEY : defaultKey()
|
||||
const key = modelKey(m.providerID, m.id)
|
||||
const favorite = favoriteKey(m)
|
||||
if (!search() && favoriteKeys().has(key) && rowMap().has(favorite)) return favorite
|
||||
if (!hasSearch() && favoriteKeys().has(key) && rowMap().has(favorite)) return favorite
|
||||
return canonicalKey(m)
|
||||
}
|
||||
const chosen = (row: ModelRow) => {
|
||||
@@ -461,15 +512,17 @@ export const ModelSelectorBase: Component<ModelSelectorBaseProps> = (props) => {
|
||||
const match = list[0]
|
||||
const first = match ? canonicalKey(match) : null
|
||||
const next =
|
||||
canon && rowMap().has(canon)
|
||||
? canon
|
||||
: first && rowMap().has(first)
|
||||
? first
|
||||
: props.allowClear
|
||||
? CLEAR_KEY
|
||||
: defaultKey()
|
||||
hasSearch() && first
|
||||
? first
|
||||
: canon && rowMap().has(canon)
|
||||
? canon
|
||||
: first && rowMap().has(first)
|
||||
? first
|
||||
: props.allowClear
|
||||
? CLEAR_KEY
|
||||
: defaultKey()
|
||||
setSelectedKey(next)
|
||||
setBrowsing(!!search() && nodeMap().has(next))
|
||||
setBrowsing(hasSearch() && (!!first || props.allowClear === true))
|
||||
setNavigating(false)
|
||||
setPreActiveKey(next)
|
||||
setPreviewKey(next)
|
||||
@@ -612,9 +665,10 @@ export const ModelSelectorBase: Component<ModelSelectorBaseProps> = (props) => {
|
||||
}
|
||||
|
||||
function horizontal(step: -1 | 1) {
|
||||
if (hasSearch()) return
|
||||
const node = nodeMap().get(selectedKey())
|
||||
if (!node) return
|
||||
if (node.kind === "group" && node.group) {
|
||||
if (node.kind === "group" && node.group && node.group.label) {
|
||||
if (step === -1 && isGroupOpen(node.group.key)) {
|
||||
toggleGroup(node.group.key)
|
||||
return
|
||||
@@ -925,7 +979,7 @@ export const ModelSelectorBase: Component<ModelSelectorBaseProps> = (props) => {
|
||||
<path d="M4 6l4 5 4-5H4z" />
|
||||
</svg>
|
||||
<span>{group.label}</span>
|
||||
<Show when={!shown() && !!search()}>
|
||||
<Show when={!shown() && hasSearch()}>
|
||||
<span class="model-selector-group-match-dot" aria-hidden="true" />
|
||||
</Show>
|
||||
</div>
|
||||
@@ -957,7 +1011,7 @@ export const ModelSelectorBase: Component<ModelSelectorBaseProps> = (props) => {
|
||||
const hovered = () => isSelected(row.key)
|
||||
const preActive = () => isPreActive(row.key)
|
||||
const starred = () => favoriteKeys().has(modelKey(model.providerID, model.id))
|
||||
const showProvider = () => row.kind === "favorite"
|
||||
const showProvider = () => row.kind === "favorite" || hasSearch()
|
||||
const showSelect = () => expanded() && preActive() && !isActive(model)
|
||||
const starLabel = () =>
|
||||
`${starred() ? language.t("model.favorite.remove") : language.t("model.favorite.add")}: ${sanitizeName(model.name)}`
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { ModelSelection } from "../../types/messages"
|
||||
import type { ModelSelection, ModelUsageMap } from "../../types/messages"
|
||||
import type { EnrichedModel } from "../../context/provider"
|
||||
import { searchMatch } from "../../utils/search-match"
|
||||
import {
|
||||
KILO_PROVIDER_ID as KILO_GATEWAY_ID,
|
||||
PROVIDER_PRIORITY as PROVIDER_ORDER,
|
||||
@@ -68,6 +69,140 @@ export function freeDataLabel(_free: string, data: string): string {
|
||||
return data
|
||||
}
|
||||
|
||||
export function modelSelectionKey(providerID: string, modelID: string): string {
|
||||
return `${providerID}/${modelID}`
|
||||
}
|
||||
|
||||
function collapse(value: string): string {
|
||||
return value.toLowerCase().replace(/[^a-z0-9]+/g, "")
|
||||
}
|
||||
|
||||
function words(value: string): string[] {
|
||||
return value
|
||||
.toLowerCase()
|
||||
.split(/[^a-z0-9]+/)
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
function tokenScore(token: string, value: string): number {
|
||||
const list = words(value)
|
||||
if (list.includes(token)) return 1000
|
||||
if (list.some((word) => word.startsWith(token))) return 700
|
||||
if (collapse(value).includes(collapse(token))) return 400
|
||||
if (searchMatch(token, value)) return 250
|
||||
return -1
|
||||
}
|
||||
|
||||
function matchScore(model: EnrichedModel, query: string): number | undefined {
|
||||
const name = stripSubProviderPrefix(sanitizeName(model.name))
|
||||
const tokens = words(query)
|
||||
if (tokens.length === 0) return 0
|
||||
|
||||
const scores = tokens.map((token) => {
|
||||
const modelScore = Math.max(tokenScore(token, name), tokenScore(token, model.id))
|
||||
const providerScore = modelScore < 0 ? tokenScore(token, model.providerName) : -1
|
||||
return { modelScore, providerScore }
|
||||
})
|
||||
if (scores.some((score) => score.modelScore < 0 && score.providerScore < 0)) return undefined
|
||||
|
||||
const modelScore = scores.reduce((sum, score) => sum + Math.max(score.modelScore, 0), 0)
|
||||
const providerScore = scores.reduce((sum, score) => sum + Math.max(score.providerScore, 0), 0)
|
||||
const exact = collapse(query) === collapse(name) || collapse(query) === collapse(model.id)
|
||||
return modelScore + Math.floor(providerScore / 10) + (exact ? 5000 : 0)
|
||||
}
|
||||
|
||||
function logicalModelKey(model: EnrichedModel): string {
|
||||
return collapse(stripSubProviderPrefix(sanitizeName(model.name))) || collapse(model.id)
|
||||
}
|
||||
|
||||
function usageFor(model: EnrichedModel, usage: ModelUsageMap | undefined) {
|
||||
return usage?.[modelSelectionKey(model.providerID, model.id)] ?? { count: 0, lastUsed: 0 }
|
||||
}
|
||||
|
||||
export interface ModelSearchOptions {
|
||||
usage?: ModelUsageMap
|
||||
favorites?: ReadonlySet<string>
|
||||
recent?: readonly ModelSelection[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Ranks matching models globally instead of sorting each provider independently.
|
||||
* Exact model tokens beat prefixes such as "sol" in "solar", while personal
|
||||
* usage only breaks ties between similarly relevant matches.
|
||||
*/
|
||||
export function rankModelSearch(
|
||||
models: readonly EnrichedModel[],
|
||||
query: string,
|
||||
options: ModelSearchOptions = {},
|
||||
): EnrichedModel[] {
|
||||
const groups = new Map<
|
||||
string,
|
||||
{
|
||||
key: string
|
||||
score: number
|
||||
count: number
|
||||
lastUsed: number
|
||||
items: Array<{ model: EnrichedModel; score: number; count: number; lastUsed: number }>
|
||||
}
|
||||
>()
|
||||
const recent = new Map(
|
||||
(options.recent ?? []).map((item, index) => [modelSelectionKey(item.providerID, item.modelID), index]),
|
||||
)
|
||||
|
||||
for (const model of models) {
|
||||
const score = matchScore(model, query)
|
||||
if (score === undefined) continue
|
||||
const usage = usageFor(model, options.usage)
|
||||
const key = logicalModelKey(model)
|
||||
const group = groups.get(key) ?? { key, score, count: 0, lastUsed: 0, items: [] }
|
||||
group.score = Math.max(group.score, score)
|
||||
group.count += usage.count
|
||||
group.lastUsed = Math.max(group.lastUsed, usage.lastUsed)
|
||||
group.items.push({ model, score, count: usage.count, lastUsed: usage.lastUsed })
|
||||
groups.set(key, group)
|
||||
}
|
||||
|
||||
return [...groups.values()]
|
||||
.sort((a, b) => b.score - a.score || b.count - a.count || b.lastUsed - a.lastUsed || a.key.localeCompare(b.key))
|
||||
.flatMap((group) =>
|
||||
group.items
|
||||
.sort(
|
||||
(a, b) =>
|
||||
b.score - a.score ||
|
||||
b.count - a.count ||
|
||||
b.lastUsed - a.lastUsed ||
|
||||
(options.favorites?.has(modelSelectionKey(b.model.providerID, b.model.id)) ? 1 : 0) -
|
||||
(options.favorites?.has(modelSelectionKey(a.model.providerID, a.model.id)) ? 1 : 0) ||
|
||||
(recent.get(modelSelectionKey(a.model.providerID, a.model.id)) ?? Infinity) -
|
||||
(recent.get(modelSelectionKey(b.model.providerID, b.model.id)) ?? Infinity) ||
|
||||
providerSortKey(a.model.providerID) - providerSortKey(b.model.providerID) ||
|
||||
a.model.providerName.localeCompare(b.model.providerName) ||
|
||||
a.model.name.localeCompare(b.model.name) ||
|
||||
a.model.id.localeCompare(b.model.id),
|
||||
)
|
||||
.map((item) => item.model),
|
||||
)
|
||||
}
|
||||
|
||||
export function mostUsedModels(
|
||||
models: readonly EnrichedModel[],
|
||||
usage: ModelUsageMap | undefined,
|
||||
favorites: ReadonlySet<string> = new Set(),
|
||||
limit = 5,
|
||||
): EnrichedModel[] {
|
||||
return models
|
||||
.filter((model) => {
|
||||
const item = usageFor(model, usage)
|
||||
return item.count > 0 && !favorites.has(modelSelectionKey(model.providerID, model.id))
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const left = usageFor(a, usage)
|
||||
const right = usageFor(b, usage)
|
||||
return right.count - left.count || right.lastUsed - left.lastUsed || a.name.localeCompare(b.name)
|
||||
})
|
||||
.slice(0, limit)
|
||||
}
|
||||
|
||||
// Strips trailing "(free)" parenthesized suffix from model display names, e.g.
|
||||
// "Llama 3 (free)" → "Llama 3". A separate "Free" label/tag is rendered
|
||||
// elsewhere, so preserve bare trailing "Free" words (e.g. "Kilo Auto Free").
|
||||
|
||||
@@ -40,6 +40,7 @@ import type {
|
||||
SuggestionRequest,
|
||||
TodoItem,
|
||||
ModelSelection,
|
||||
ModelUsageMap,
|
||||
ContextUsage,
|
||||
AgentInfo,
|
||||
SkillInfo,
|
||||
@@ -122,6 +123,7 @@ interface SessionStore {
|
||||
variantSelections: Record<string, string> // session/agent scoped variant key -> variant name
|
||||
recentModels: ModelSelection[]
|
||||
favoriteModels: ModelSelection[]
|
||||
modelUsageHistory: ModelUsageMap
|
||||
modelUsage: Record<string, { requestID: string; data?: SessionModelUsage }>
|
||||
}
|
||||
|
||||
@@ -244,6 +246,8 @@ interface SessionContextValue {
|
||||
selectVariant: (value: string, sessionID?: string) => void
|
||||
|
||||
// Model favorites
|
||||
recentModels: Accessor<ModelSelection[]>
|
||||
modelUsageHistory: Accessor<ModelUsageMap>
|
||||
favoriteModels: Accessor<ModelSelection[]>
|
||||
toggleFavorite: (providerID: string, modelID: string) => void
|
||||
|
||||
@@ -511,7 +515,6 @@ export const SessionProvider: ParentComponent = (props) => {
|
||||
)
|
||||
}
|
||||
|
||||
// Store for sessions, messages, parts, todos, modelSelections, agentSelections
|
||||
const [store, setStore] = createStore<SessionStore>({
|
||||
sessions: {},
|
||||
messages: {},
|
||||
@@ -524,6 +527,7 @@ export const SessionProvider: ParentComponent = (props) => {
|
||||
variantSelections: {},
|
||||
recentModels: [],
|
||||
favoriteModels: [],
|
||||
modelUsageHistory: {},
|
||||
modelUsage: {},
|
||||
})
|
||||
const [modelUsageReady, setModelUsageReady] = createSignal(false)
|
||||
@@ -632,6 +636,14 @@ export const SessionProvider: ParentComponent = (props) => {
|
||||
vscode.postMessage({ type: "persistRecents", recents: updated })
|
||||
}
|
||||
|
||||
function recordModelUsage(providerID?: string, modelID?: string) {
|
||||
if (!providerID || !modelID) return
|
||||
const key = `${providerID}/${modelID}`
|
||||
const current = store.modelUsageHistory[key] ?? { count: 0, lastUsed: 0 }
|
||||
setStore("modelUsageHistory", key, { count: current.count + 1, lastUsed: Date.now() })
|
||||
vscode.postMessage({ type: "recordModelUsage", providerID, modelID })
|
||||
}
|
||||
|
||||
function applyModel(agentName: string, selection: ModelSelection, sessionID?: string) {
|
||||
pushRecent(selection)
|
||||
if (sessionID) {
|
||||
@@ -965,6 +977,12 @@ export const SessionProvider: ParentComponent = (props) => {
|
||||
vscode.postMessage({ type: "requestRecents" })
|
||||
onCleanup(unsubRecents)
|
||||
|
||||
const unsubModelUsage = vscode.onMessage((message: ExtensionMessage) => {
|
||||
if (message.type !== "modelUsageLoaded") return
|
||||
setStore("modelUsageHistory", message.usage)
|
||||
})
|
||||
vscode.postMessage({ type: "requestModelUsage" })
|
||||
onCleanup(unsubModelUsage)
|
||||
// Load persisted favorite models from extension globalState
|
||||
const unsubFavorites = vscode.onMessage((message: ExtensionMessage) => {
|
||||
if (message.type !== "favoritesLoaded") return
|
||||
@@ -2288,6 +2306,8 @@ export const SessionProvider: ParentComponent = (props) => {
|
||||
const messageID = Identifier.ascending("message")
|
||||
|
||||
const sid = origin === undefined ? currentSessionID() : (origin ?? undefined)
|
||||
const selection = providerID && modelID ? { providerID, modelID } : selected(sid)
|
||||
recordModelUsage(selection?.providerID, selection?.modelID)
|
||||
const preview = sid?.startsWith("cloud:")
|
||||
? sid.slice("cloud:".length)
|
||||
: origin === undefined
|
||||
@@ -2364,6 +2384,8 @@ export const SessionProvider: ParentComponent = (props) => {
|
||||
|
||||
// Cloud previews need import-then-command; post importAndSend with command metadata
|
||||
const sid = origin === undefined ? currentSessionID() : (origin ?? undefined)
|
||||
const selection = providerID && modelID ? { providerID, modelID } : selected(sid)
|
||||
recordModelUsage(selection?.providerID, selection?.modelID)
|
||||
const preview = sid?.startsWith("cloud:")
|
||||
? sid.slice("cloud:".length)
|
||||
: origin === undefined
|
||||
@@ -3025,6 +3047,8 @@ export const SessionProvider: ParentComponent = (props) => {
|
||||
allMessages,
|
||||
allParts,
|
||||
allStatusMap,
|
||||
recentModels: () => store.recentModels,
|
||||
modelUsageHistory: () => store.modelUsageHistory,
|
||||
favoriteModels: () => store.favoriteModels,
|
||||
toggleFavorite,
|
||||
variantList,
|
||||
|
||||
+1
@@ -158,6 +158,7 @@ export const dict = {
|
||||
"model.group.auto": "النماذج التلقائية",
|
||||
"model.group.recommended": "موصى به",
|
||||
"model.group.favorites": "المفضلة",
|
||||
"model.group.mostUsed": "الأكثر استخدامًا",
|
||||
"model.favorite.add": "إضافة إلى المفضلة",
|
||||
"model.favorite.remove": "إزالة من المفضلة",
|
||||
"model.preview.label.released": "الإصدار",
|
||||
|
||||
+1
@@ -162,6 +162,7 @@ export const dict = {
|
||||
"model.group.auto": "Modelos automáticos",
|
||||
"model.group.recommended": "Recomendado",
|
||||
"model.group.favorites": "Favoritos",
|
||||
"model.group.mostUsed": "Mais usados",
|
||||
"model.favorite.add": "Adicionar aos favoritos",
|
||||
"model.favorite.remove": "Remover dos favoritos",
|
||||
"model.preview.label.released": "Lançado",
|
||||
|
||||
+1
@@ -163,6 +163,7 @@ export const dict = {
|
||||
"model.group.auto": "Automatski modeli",
|
||||
"model.group.recommended": "Preporučeno",
|
||||
"model.group.favorites": "Favoriti",
|
||||
"model.group.mostUsed": "Najčešće korišteni",
|
||||
"model.favorite.add": "Dodaj u favorite",
|
||||
"model.favorite.remove": "Ukloni iz favorita",
|
||||
"model.preview.label.released": "Objavljeno",
|
||||
|
||||
+1
@@ -162,6 +162,7 @@ export const dict = {
|
||||
"model.group.auto": "Automatiske modeller",
|
||||
"model.group.recommended": "Anbefalet",
|
||||
"model.group.favorites": "Favoritter",
|
||||
"model.group.mostUsed": "Mest brugte",
|
||||
"model.favorite.add": "Føj til favoritter",
|
||||
"model.favorite.remove": "Fjern fra favoritter",
|
||||
|
||||
|
||||
@@ -169,6 +169,7 @@ export const dict = {
|
||||
"model.group.auto": "Automatische Modelle",
|
||||
"model.group.recommended": "Empfohlen",
|
||||
"model.group.favorites": "Favoriten",
|
||||
"model.group.mostUsed": "Am häufigsten verwendet",
|
||||
"model.favorite.add": "Zu Favoriten hinzufügen",
|
||||
"model.favorite.remove": "Aus Favoriten entfernen",
|
||||
|
||||
|
||||
@@ -162,6 +162,7 @@ export const dict = {
|
||||
"model.group.auto": "Auto Models",
|
||||
"model.group.recommended": "Recommended",
|
||||
"model.group.favorites": "Favorites",
|
||||
"model.group.mostUsed": "Most used",
|
||||
"model.favorite.add": "Add to favorites",
|
||||
"model.favorite.remove": "Remove from favorites",
|
||||
"model.preview.label.released": "Released",
|
||||
|
||||
+1
@@ -164,6 +164,7 @@ export const dict = {
|
||||
"model.group.auto": "Modelos automáticos",
|
||||
"model.group.recommended": "Recomendado",
|
||||
"model.group.favorites": "Favoritos",
|
||||
"model.group.mostUsed": "Más usados",
|
||||
"model.favorite.add": "Añadir a favoritos",
|
||||
"model.favorite.remove": "Eliminar de favoritos",
|
||||
|
||||
|
||||
+1
@@ -163,6 +163,7 @@ export const dict = {
|
||||
"model.group.auto": "مدلهای خودکار",
|
||||
"model.group.recommended": "پیشنهادی",
|
||||
"model.group.favorites": "موردعلاقهها",
|
||||
"model.group.mostUsed": "پراستفادهترین",
|
||||
"model.favorite.add": "افزودن به موردعلاقهها",
|
||||
"model.favorite.remove": "حذف از موردعلاقهها",
|
||||
"model.preview.label.released": "منتشر شده",
|
||||
|
||||
+1
@@ -163,6 +163,7 @@ export const dict = {
|
||||
"model.group.auto": "Modèles automatiques",
|
||||
"model.group.recommended": "Recommandé",
|
||||
"model.group.favorites": "Favoris",
|
||||
"model.group.mostUsed": "Les plus utilisés",
|
||||
"model.favorite.add": "Ajouter aux favoris",
|
||||
"model.favorite.remove": "Retirer des favoris",
|
||||
|
||||
|
||||
+1
@@ -120,6 +120,7 @@ export const dict = {
|
||||
"model.group.auto": "Modelli automatici",
|
||||
"model.group.recommended": "Consigliati",
|
||||
"model.group.favorites": "Preferiti",
|
||||
"model.group.mostUsed": "Più usati",
|
||||
"model.favorite.add": "Aggiungi ai preferiti",
|
||||
"model.favorite.remove": "Rimuovi dai preferiti",
|
||||
"model.preview.label.released": "Rilasciato",
|
||||
|
||||
+1
@@ -162,6 +162,7 @@ export const dict = {
|
||||
"model.group.auto": "自動モデル",
|
||||
"model.group.recommended": "推奨",
|
||||
"model.group.favorites": "お気に入り",
|
||||
"model.group.mostUsed": "よく使うモデル",
|
||||
"model.favorite.add": "お気に入りに追加",
|
||||
"model.favorite.remove": "お気に入りから削除",
|
||||
|
||||
|
||||
+1
@@ -166,6 +166,7 @@ export const dict = {
|
||||
"model.group.auto": "자동 모델",
|
||||
"model.group.recommended": "추천",
|
||||
"model.group.favorites": "즐겨찾기",
|
||||
"model.group.mostUsed": "가장 많이 사용됨",
|
||||
"model.favorite.add": "즐겨찾기에 추가",
|
||||
"model.favorite.remove": "즐겨찾기에서 제거",
|
||||
|
||||
|
||||
+1
@@ -164,6 +164,7 @@ export const dict = {
|
||||
"model.group.auto": "Automatische modellen",
|
||||
"model.group.recommended": "Aanbevolen",
|
||||
"model.group.favorites": "Favorieten",
|
||||
"model.group.mostUsed": "Meest gebruikt",
|
||||
"model.favorite.add": "Toevoegen aan favorieten",
|
||||
"model.favorite.remove": "Verwijderen uit favorieten",
|
||||
"model.preview.label.released": "Uitgebracht",
|
||||
|
||||
+1
@@ -165,6 +165,7 @@ export const dict = {
|
||||
"model.group.auto": "Automatiske modeller",
|
||||
"model.group.recommended": "Anbefalt",
|
||||
"model.group.favorites": "Favoritter",
|
||||
"model.group.mostUsed": "Mest brukt",
|
||||
"model.favorite.add": "Legg til i favoritter",
|
||||
"model.favorite.remove": "Fjern fra favoritter",
|
||||
"model.preview.label.released": "Utgitt",
|
||||
|
||||
+1
@@ -162,6 +162,7 @@ export const dict = {
|
||||
"model.group.auto": "Modele automatyczne",
|
||||
"model.group.recommended": "Zalecane",
|
||||
"model.group.favorites": "Ulubione",
|
||||
"model.group.mostUsed": "Najczęściej używane",
|
||||
"model.favorite.add": "Dodaj do ulubionych",
|
||||
"model.favorite.remove": "Usuń z ulubionych",
|
||||
"model.preview.label.released": "Wydano",
|
||||
|
||||
+1
@@ -162,6 +162,7 @@ export const dict = {
|
||||
"model.group.auto": "Автоматические модели",
|
||||
"model.group.recommended": "Рекомендуемые",
|
||||
"model.group.favorites": "Избранное",
|
||||
"model.group.mostUsed": "Часто используемые",
|
||||
"model.favorite.add": "Добавить в избранное",
|
||||
"model.favorite.remove": "Удалить из избранного",
|
||||
"model.preview.label.released": "Выпущена",
|
||||
|
||||
+1
@@ -161,6 +161,7 @@ export const dict = {
|
||||
"model.group.auto": "โมเดลอัตโนมัติ",
|
||||
"model.group.recommended": "แนะนำ",
|
||||
"model.group.favorites": "รายการโปรด",
|
||||
"model.group.mostUsed": "ใช้บ่อยที่สุด",
|
||||
"model.favorite.add": "เพิ่มในรายการโปรด",
|
||||
"model.favorite.remove": "ลบออกจากรายการโปรด",
|
||||
"model.preview.label.released": "เปิดตัว",
|
||||
|
||||
+1
@@ -162,6 +162,7 @@ export const dict = {
|
||||
"model.group.auto": "Otomatik Modeller",
|
||||
"model.group.recommended": "Önerilen",
|
||||
"model.group.favorites": "Favoriler",
|
||||
"model.group.mostUsed": "En çok kullanılan",
|
||||
"model.favorite.add": "Favorilere ekle",
|
||||
"model.favorite.remove": "Favorilerden çıkar",
|
||||
"model.preview.label.released": "Yayınlanma",
|
||||
|
||||
+1
@@ -163,6 +163,7 @@ export const dict = {
|
||||
"model.group.auto": "Автоматичні моделі",
|
||||
"model.group.recommended": "Рекомендовані",
|
||||
"model.group.favorites": "Обране",
|
||||
"model.group.mostUsed": "Найчастіше використовувані",
|
||||
"model.favorite.add": "Додати до обраного",
|
||||
"model.favorite.remove": "Видалити з обраного",
|
||||
"model.preview.label.released": "Випущено",
|
||||
|
||||
+1
@@ -158,6 +158,7 @@ export const dict = {
|
||||
"model.group.auto": "自动模型",
|
||||
"model.group.recommended": "推荐",
|
||||
"model.group.favorites": "收藏夹",
|
||||
"model.group.mostUsed": "最常用",
|
||||
"model.favorite.add": "添加到收藏夹",
|
||||
"model.favorite.remove": "从收藏夹中移除",
|
||||
|
||||
|
||||
+1
@@ -158,6 +158,7 @@ export const dict = {
|
||||
"model.group.auto": "自動模型",
|
||||
"model.group.recommended": "推薦",
|
||||
"model.group.favorites": "我的最愛",
|
||||
"model.group.mostUsed": "最常用",
|
||||
"model.favorite.add": "加入我的最愛",
|
||||
"model.favorite.remove": "從我的最愛中移除",
|
||||
|
||||
|
||||
@@ -254,6 +254,8 @@ export function mockSessionValue(overrides?: {
|
||||
revertSession: noop,
|
||||
unrevertSession: noop,
|
||||
favoriteModels: () => [],
|
||||
recentModels: () => [],
|
||||
modelUsageHistory: () => ({}),
|
||||
toggleFavorite: noop,
|
||||
variantList: () => [],
|
||||
currentVariant: () => undefined,
|
||||
|
||||
@@ -131,6 +131,28 @@ export const ModelSelectorSelectedFavorite: Story = {
|
||||
},
|
||||
}
|
||||
|
||||
export const ModelSelectorMostUsed: Story = {
|
||||
name: "ModelSelector - most used suggestions",
|
||||
render: () => {
|
||||
const session = {
|
||||
...mockSessionValue(),
|
||||
modelUsageHistory: () => ({
|
||||
"kilo/alpha": { count: 3, lastUsed: 100 },
|
||||
"kilo/bravo": { count: 12, lastUsed: 200 },
|
||||
"nvidia/nova": { count: 7, lastUsed: 300 },
|
||||
}),
|
||||
}
|
||||
|
||||
return (
|
||||
<StoryProviders>
|
||||
<SessionContext.Provider value={session as any}>
|
||||
<AccessibleModelSelector />
|
||||
</SessionContext.Provider>
|
||||
</StoryProviders>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
const LARGE_MODELS: EnrichedModel[] = Array.from({ length: 600 }, (_, i) => {
|
||||
const id = String(i).padStart(3, "0")
|
||||
const provider = `provider-${i % 12}`
|
||||
|
||||
@@ -18,7 +18,7 @@ import type { AgentManagerSidebarTarget } from "./webview-messages"
|
||||
import type { PermissionRequest } from "./permissions"
|
||||
import type { AnacondaDesktopExtensionMessage } from "../../../../src/shared/anaconda-desktop-messages"
|
||||
import type { QuestionRequest, SuggestionRequest, TodoItem } from "./questions"
|
||||
import type { ModelSelection, Provider, ProviderAuthState } from "./providers"
|
||||
import type { ModelSelection, ModelUsageMap, Provider, ProviderAuthState } from "./providers"
|
||||
import type { SpeechToTextModelDef } from "../../../../src/speech-to-text/models"
|
||||
import type { AgentInfo, AgentRequirementResult, SkillInfo, SlashCommandInfo } from "./agents"
|
||||
import type {
|
||||
@@ -920,6 +920,11 @@ export interface RecentsLoadedMessage {
|
||||
recents: ModelSelection[]
|
||||
}
|
||||
|
||||
export interface ModelUsageLoadedMessage {
|
||||
type: "modelUsageLoaded"
|
||||
usage: ModelUsageMap
|
||||
}
|
||||
|
||||
// Persisted model-selector expand/collapse preference (extension → webview)
|
||||
export interface ModelSelectorExpandedLoadedMessage {
|
||||
type: "modelSelectorExpandedLoaded"
|
||||
@@ -1298,6 +1303,7 @@ export type ExtensionMessage =
|
||||
| MessagesLoadedMessage
|
||||
| SessionModelUsageLoadedMessage
|
||||
| SessionModelUsageChangedMessage
|
||||
| ModelUsageLoadedMessage
|
||||
| MessageCreatedMessage
|
||||
| SessionsLoadedMessage
|
||||
| CloudSessionsLoadedMessage
|
||||
|
||||
@@ -53,6 +53,13 @@ export interface ModelSelection {
|
||||
modelID: string
|
||||
}
|
||||
|
||||
export interface ModelUsage {
|
||||
count: number
|
||||
lastUsed: number
|
||||
}
|
||||
|
||||
export type ModelUsageMap = Record<string, ModelUsage>
|
||||
|
||||
export type ProviderAuthState = "api" | "oauth" | "wellknown"
|
||||
|
||||
export interface ProviderConfig {
|
||||
|
||||
@@ -1242,6 +1242,16 @@ export interface RequestRecentsMessage {
|
||||
type: "requestRecents"
|
||||
}
|
||||
|
||||
export interface RecordModelUsageMessage {
|
||||
type: "recordModelUsage"
|
||||
providerID: string
|
||||
modelID: string
|
||||
}
|
||||
|
||||
export interface RequestModelUsageMessage {
|
||||
type: "requestModelUsage"
|
||||
}
|
||||
|
||||
export interface PersistModelSelectorExpandedRequest {
|
||||
type: "persistModelSelectorExpanded"
|
||||
value: boolean
|
||||
@@ -1559,6 +1569,8 @@ export type WebviewMessage =
|
||||
| FetchCustomProviderModelsMessage
|
||||
| PersistRecentsRequest
|
||||
| RequestRecentsMessage
|
||||
| RecordModelUsageMessage
|
||||
| RequestModelUsageMessage
|
||||
| PersistModelSelectorExpandedRequest
|
||||
| RequestModelSelectorExpandedMessage
|
||||
| ToggleFavoriteRequest
|
||||
|
||||
Reference in New Issue
Block a user