1import { normalizeSearchQuery, scoreQueryMatch } from "@t3tools/shared/searchRanking";
2
3type ModelPickerSearchableModel = {
4 /** Driver kind — indexed so "codex" still matches a Codex Personal instance. */
5 driverKind: string;
6 /**
7 * Instance display name (e.g. "Codex Personal"). Indexed as a search
8 * field so typing the custom instance's user-authored name matches its
9 * models directly instead of just the driver kind.
10 */
11 providerDisplayName: string;
12 name: string;
13 shortName?: string;
14 subProvider?: string;
15 isFavorite?: boolean;
16};
17
18const MODEL_PICKER_FAVORITE_SCORE_BOOST = 24;
19
20function getModelPickerSearchFields(model: ModelPickerSearchableModel): string[] {
21 return [
22 normalizeSearchQuery(model.name),
23 ...(model.shortName ? [normalizeSearchQuery(model.shortName)] : []),
24 ...(model.subProvider ? [normalizeSearchQuery(model.subProvider)] : []),
25 normalizeSearchQuery(model.driverKind),
26 normalizeSearchQuery(model.providerDisplayName),
27 buildModelPickerSearchText(model),
28 ];
29}
30
31function scoreModelPickerSearchToken(
32 field: string,
33 token: string,
34 fieldBase: number,
35): number | null {
36 return scoreQueryMatch({
37 value: field,
38 query: token,
39 exactBase: fieldBase,
40 prefixBase: fieldBase + 2,
41 boundaryBase: fieldBase + 4,
42 includesBase: fieldBase + 6,
43 ...(token.length >= 3 ? { fuzzyBase: fieldBase + 100 } : {}),
44 });
45}
46
47export function buildModelPickerSearchText(model: ModelPickerSearchableModel): string {
48 return normalizeSearchQuery(
49 [model.name, model.shortName, model.subProvider, model.driverKind, model.providerDisplayName]
50 .filter((value): value is string => typeof value === "string" && value.length > 0)
51 .join(" "),
52 );
53}
54
55export function scoreModelPickerSearch(
56 model: ModelPickerSearchableModel,
57 query: string,
58): number | null {
59 const tokens = normalizeSearchQuery(query)
60 .split(/\s+/u)
61 .filter((token) => token.length > 0);
62
63 if (tokens.length === 0) {
64 return 0;
65 }
66
67 const fields = getModelPickerSearchFields(model);
68 let score = 0;
69
70 for (const token of tokens) {
71 const tokenScores: Array<number> = [];
72 for (let index = 0; index < fields.length; index += 1) {
73 const fieldScore = scoreModelPickerSearchToken(fields[index]!, token, index * 10);
74 if (fieldScore !== null) {
75 tokenScores.push(fieldScore);
76 }
77 }
78
79 if (tokenScores.length === 0) {
80 return null;
81 }
82
83 score += Math.min(...tokenScores);
84 }
85
86 return model.isFavorite ? score - MODEL_PICKER_FAVORITE_SCORE_BOOST : score;
87}