fix(models): match bare Kimi Coding k3 when searching kimi

Kimi Coding discovers the flagship as wire id `k3`. Picker search used
only that id, so typing "kimi" hid it next to every other kimi-* model.
Add picker-only search aliases without changing the wire id.
This commit is contained in:
HexLab98 2026-07-19 16:55:51 +07:00 committed by Teknium
parent 2be2464d25
commit c63e0cd3e3
9 changed files with 151 additions and 6 deletions

View file

@ -4,6 +4,7 @@ import { useState } from 'react'
import { useI18n } from '@/i18n'
import { modelOptionsQueryKey, requestModelOptions } from '@/lib/model-options'
import { currentPickerSelection } from '@/lib/model-status-label'
import { modelSearchText } from '@/lib/model-search-text'
import { normalize } from '@/lib/text'
import type { ModelOptionProvider, ModelPricing } from '@/types/hermes'
@ -172,7 +173,7 @@ function ModelResults({
const matches = (provider: ModelOptionProvider, model: string) =>
!q ||
model.toLowerCase().includes(q) ||
modelSearchText(model).toLowerCase().includes(q) ||
provider.name.toLowerCase().includes(q) ||
provider.slug.toLowerCase().includes(q)

View file

@ -0,0 +1,28 @@
/**
* Extra tokens used only for model-picker search ranking.
*
* Wire IDs stay unchanged some providers report short or brand-less ids
* (Kimi Coding's flagship is literally `k3`) that users still search for by
* the familiar `kimi-…` naming of sibling models.
*
* Keep in sync with ui-tui/src/lib/model-search-text.ts,
* web/src/lib/model-search-text.ts, and hermes_cli/model_search.py.
*/
const MODEL_SEARCH_ALIASES: Record<string, readonly string[]> = {
k3: ['kimi-k3', 'kimi']
}
/** Haystack for fuzzy/substring model search; never changes the wire id. */
export function modelSearchText(model: string): string {
const id = model.trim()
if (!id) {
return model
}
const aliases = MODEL_SEARCH_ALIASES[id.toLowerCase()]
if (!aliases?.length) {
return id
}
return `${id} ${aliases.join(' ')}`
}

View file

@ -7341,6 +7341,22 @@ def _prompt_model_selection(
desc_lines.append(f" ── {unavailable_footer} ──")
description = "\n".join(desc_lines) if desc_lines else None
# Search haystacks keep pricing labels visible while adding aliases
# for brand-less wire ids (e.g. Kimi Coding `k3` ↔ query "kimi").
from hermes_cli.model_search import model_search_text
model_search_labels = []
for mid in ordered:
label = _label(mid)
haystack = model_search_text(mid)
# model_search_text always starts with the wire id; only append when
# aliases add tokens beyond the bare id already in the label.
model_search_labels.append(
label if haystack == mid else f"{label} {haystack}"
)
model_search_labels.append("Enter custom model name")
model_search_labels.append("Skip (keep current)")
idx = curses_radiolist(
"Select default model:",
choices,
@ -7348,6 +7364,7 @@ def _prompt_model_selection(
cancel_returns=-1,
description=description,
searchable=True,
search_labels=model_search_labels,
)
if idx < 0:
return None

View file

@ -723,6 +723,7 @@ def curses_radiolist(
cancel_returns: int | None = None,
description: str | None = None,
searchable: bool = False,
search_labels: List[str] | None = None,
) -> int:
"""Curses single-select radio list. Returns the selected index.
@ -741,6 +742,8 @@ def curses_radiolist(
searchable: When true, ``/`` opens a type-to-filter prompt. The
returned value is always the original item index, not a filtered
row position.
search_labels: Optional haystacks for type-to-filter (length must
match ``items``). Defaults to the display labels when omitted.
"""
if cancel_returns is None:
cancel_returns = selected
@ -812,7 +815,11 @@ def curses_radiolist(
fallback=lambda: _radio_numbered_fallback(title, items, selected, cancel_returns),
cancel_value=cancel_returns,
searchable=searchable,
search_labels=plain_labels if searchable else None,
search_labels=(
list(search_labels)
if searchable and search_labels is not None
else (plain_labels if searchable else None)
),
)

View file

@ -0,0 +1,30 @@
"""Picker-only search aliases for model ids.
Wire IDs stay unchanged. Some providers report short or brand-less ids
(Kimi Coding's flagship is literally ``k3``) that users still search for by
the familiar ``kimi-`` naming of sibling models.
Keep in sync with ``ui-tui/src/lib/model-search-text.ts`` and
``web/src/lib/model-search-text.ts``.
"""
from __future__ import annotations
# Lowercased wire id → extra tokens appended to the search haystack only.
_MODEL_SEARCH_ALIASES: dict[str, tuple[str, ...]] = {
"k3": ("kimi-k3", "kimi"),
}
def model_search_text(model: str) -> str:
"""Return the haystack used for fuzzy/substring model search.
Never changes the wire id passed to the provider.
"""
mid = (model or "").strip()
if not mid:
return model or ""
aliases = _MODEL_SEARCH_ALIASES.get(mid.lower())
if not aliases:
return mid
return f"{mid} {' '.join(aliases)}"

View file

@ -6,6 +6,7 @@ import { TUI_SESSION_MODEL_FLAG } from '../domain/slash.js'
import type { GatewayClient } from '../gatewayClient.js'
import type { ModelOptionProvider, ModelOptionsResponse } from '../gatewayTypes.js'
import { fuzzyRank } from '../lib/fuzzy.js'
import { modelSearchText } from '../lib/model-search-text.js'
import { asRpcResult, rpcErrorMessage } from '../lib/rpc.js'
import type { Theme } from '../theme.js'
@ -136,7 +137,9 @@ export function ModelPicker({
return allModels
}
return fuzzyRank(allModels, filter, m => m).map(r => r.item)
// modelSearchText adds aliases for brand-less wire ids (e.g. Kimi
// Coding `k3` still matches a "kimi" query).
return fuzzyRank(allModels, filter, modelSearchText).map(r => r.item)
}, [allModels, filter, stage])
const models = filteredModels

View file

@ -0,0 +1,28 @@
/**
* Extra tokens used only for model-picker search ranking.
*
* Wire IDs stay unchanged some providers report short or brand-less ids
* (Kimi Coding's flagship is literally `k3`) that users still search for by
* the familiar `kimi-…` naming of sibling models.
*
* Keep in sync with web/src/lib/model-search-text.ts and
* hermes_cli/model_search.py.
*/
const MODEL_SEARCH_ALIASES: Record<string, readonly string[]> = {
k3: ['kimi-k3', 'kimi'],
}
/** Haystack for fuzzy/substring model search; never changes the wire id. */
export function modelSearchText(model: string): string {
const id = model.trim()
if (!id) {
return model
}
const aliases = MODEL_SEARCH_ALIASES[id.toLowerCase()]
if (!aliases?.length) {
return id
}
return `${id} ${aliases.join(' ')}`
}

View file

@ -12,6 +12,7 @@ import { createPortal } from "react-dom";
import { cn, themedBody } from "@/lib/utils";
import { fuzzyRank } from "@/lib/fuzzy";
import { queryMatchesProviderOnly } from "@/lib/model-picker-filter";
import { modelSearchText } from "@/lib/model-search-text";
/**
* Two-stage model picker modal.
@ -246,16 +247,18 @@ export function ModelPickerDialog(props: Props) {
);
// Fuzzy-ranked models carrying the matched character positions so the model
// list can highlight why each entry matched.
// list can highlight why each entry matched. modelSearchText adds aliases
// for brand-less wire ids (e.g. Kimi Coding `k3` ↔ search "kimi").
const filteredModels = useMemo(
() =>
fuzzyRank(
models,
queryMatchesSelectedProviderOnly ? "" : trimmedQuery,
(m) => m,
modelSearchText,
).map((r) => ({
model: r.item,
positions: r.positions,
// Positions may land in alias suffixes — keep only in-id highlights.
positions: r.positions.filter((i) => i >= 0 && i < r.item.length),
})),
[models, trimmedQuery, queryMatchesSelectedProviderOnly],
);

View file

@ -0,0 +1,28 @@
/**
* Extra tokens used only for model-picker search ranking.
*
* Wire IDs stay unchanged some providers report short or brand-less ids
* (Kimi Coding's flagship is literally `k3`) that users still search for by
* the familiar `kimi-…` naming of sibling models.
*
* Keep in sync with ui-tui/src/lib/model-search-text.ts and
* hermes_cli/model_search.py. Behavioural tests live in the TUI package.
*/
const MODEL_SEARCH_ALIASES: Record<string, readonly string[]> = {
k3: ["kimi-k3", "kimi"],
};
/** Haystack for fuzzy/substring model search; never changes the wire id. */
export function modelSearchText(model: string): string {
const id = model.trim();
if (!id) {
return model;
}
const aliases = MODEL_SEARCH_ALIASES[id.toLowerCase()];
if (!aliases?.length) {
return id;
}
return `${id} ${aliases.join(" ")}`;
}