feat(coding-agent): improved model search ranking by match quality
- Prioritized fuzzy match quality over the most-recently-used model order when filtering the model browser. - Added bucketing to match scores to ensure stable sorting when match quality is identical. - Added unit tests to verify that exact query matches take precedence over the MRU model.
This commit is contained in:
@@ -21,6 +21,7 @@
|
||||
- Fixed `/btw` side-channel turns on Codex models such as `gpt-5.6-luna` by preserving the session websocket preference instead of forcing SSE, and made Esc dismiss the active `/btw` panel before interrupting loop/maintenance work. ([#5213](https://github.com/can1357/oh-my-pi/issues/5213))
|
||||
- Fixed the Model Hub role-assignment strip hiding the selected chip once the row overflowed; the strip now scrolls horizontally, truncating passed chips behind a leading ellipsis so the selection (plus one chip of lookahead) stays visible.
|
||||
- Fixed mouse hover and clicks in the /models Roles view landing one row above the pointer (the row mapping subtracted the status row twice).
|
||||
- Fixed model search keeping the most-recently-used model on top of the results: match quality now ranks first (an exact `gpt-5.5` beats the active `gpt-5.6-sol`), with MRU order only breaking ties between equally good matches.
|
||||
|
||||
## [16.4.5] - 2026-07-11
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import { buildModel } from "@oh-my-pi/pi-catalog/build";
|
||||
import { modelsAreEqual } from "@oh-my-pi/pi-catalog/models";
|
||||
import {
|
||||
type Component,
|
||||
fuzzyFilter,
|
||||
fuzzyRank,
|
||||
Input,
|
||||
matchesKey,
|
||||
ScrollView,
|
||||
@@ -432,10 +432,19 @@ export class ModelBrowser implements Component {
|
||||
if (query.trim()) {
|
||||
// Match against the displayed "provider/id" string so the user can
|
||||
// type what they see: bare names, provider prefixes, or scoped
|
||||
// queries all flow through the same fuzzy matcher. Skip role rank
|
||||
// so a weakly matching default doesn't trump a stronger match.
|
||||
const matches = fuzzyFilter(this.#baseItems, query, ({ provider, id }) => `${provider}/${id}`);
|
||||
// queries all flow through the same fuzzy matcher.
|
||||
const ranked = fuzzyRank(this.#baseItems, query, ({ provider, id }) => `${provider}/${id}`);
|
||||
const matches = ranked.map(result => result.item);
|
||||
// Match quality is the primary key while searching: an exact
|
||||
// "gpt-5.5" must beat the MRU (or role-assigned) "gpt-5.6", so
|
||||
// role rank is skipped and MRU only breaks ties. Scores are
|
||||
// bucketed so sub-point position noise (provider-name length)
|
||||
// can't split equally good matches; within a bucket the stable
|
||||
// sort keeps sortModelItems' MRU/version order.
|
||||
sortModelItems(matches, { roles: this.#roles, mruOrder: this.#mruOrder, skipRoleRank: true });
|
||||
const buckets = new Map<ModelBrowserItem, number>();
|
||||
for (const result of ranked) buckets.set(result.item, Math.round(result.score / 10));
|
||||
matches.sort((a, b) => (buckets.get(a) ?? 0) - (buckets.get(b) ?? 0));
|
||||
items = matches;
|
||||
} else {
|
||||
items = this.#baseItems;
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import type { Model } from "@oh-my-pi/pi-ai";
|
||||
import { buildModel } from "@oh-my-pi/pi-catalog/build";
|
||||
import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings";
|
||||
import {
|
||||
buildBrowserItems,
|
||||
ModelBrowser,
|
||||
sortModelItems,
|
||||
} from "@oh-my-pi/pi-coding-agent/modes/components/model-browser";
|
||||
|
||||
function makeModel(provider: string, id: string): Model {
|
||||
return buildModel({
|
||||
id,
|
||||
name: id,
|
||||
api: "ollama-chat",
|
||||
provider,
|
||||
baseUrl: "https://example.com",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 128_000,
|
||||
maxTokens: 1024,
|
||||
});
|
||||
}
|
||||
|
||||
/** Browser preloaded with `models`, MRU-sorted like the hub does on sync. */
|
||||
function makeBrowser(models: Model[], mruOrder: string[]): ModelBrowser {
|
||||
const browser = new ModelBrowser(Settings.isolated({}));
|
||||
const items = buildBrowserItems(models);
|
||||
sortModelItems(items, { mruOrder });
|
||||
browser.setMruOrder(mruOrder);
|
||||
browser.setItems(items);
|
||||
return browser;
|
||||
}
|
||||
|
||||
describe("ModelBrowser search ranking", () => {
|
||||
test("an exact query match outranks the MRU model", () => {
|
||||
// Regression: with gpt-5.6-sol as the active (MRU) model, typing
|
||||
// "gpt-5.5" must select gpt-5.5, not keep the MRU pinned on top.
|
||||
const browser = makeBrowser(
|
||||
[
|
||||
makeModel("openai-codex", "gpt-5.6-sol"),
|
||||
makeModel("openai-codex", "gpt-5.6-luna"),
|
||||
makeModel("openai-codex", "gpt-5.5"),
|
||||
makeModel("openai-codex", "gpt-5.4"),
|
||||
],
|
||||
["openai-codex/gpt-5.6-sol", "openai-codex/gpt-5.6-luna"],
|
||||
);
|
||||
|
||||
browser.setQuery("gpt-5.5");
|
||||
|
||||
expect(browser.getSelected()?.selector).toBe("openai-codex/gpt-5.5");
|
||||
});
|
||||
|
||||
test("MRU breaks ties between equally good matches", () => {
|
||||
// Same model id under two providers: match quality is identical, so
|
||||
// the recently used provider must win over alphabetical order.
|
||||
const browser = makeBrowser([makeModel("g0i", "gpt-5.5"), makeModel("zenmux", "gpt-5.5")], ["zenmux/gpt-5.5"]);
|
||||
|
||||
browser.setQuery("gpt-5.5");
|
||||
|
||||
expect(browser.getSelected()?.selector).toBe("zenmux/gpt-5.5");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user