File size: 4,688 Bytes
f76c374
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
import { describe, expect, it } from 'vitest'

import { fuzzyRank, fuzzyScore, fuzzyScoreMulti } from './fuzzy'

describe('fuzzyScore', () => {
  it('matches a query as a subsequence (g4o → gpt-4o)', () => {
    expect(fuzzyScore('gpt-4o', 'g4o')).not.toBeNull()
    expect(fuzzyScore('gpt-4o', 'gpt')).not.toBeNull()
    expect(fuzzyScore('gpt-4o', '4o')).not.toBeNull()
  })

  it('returns null when characters are out of order or absent', () => {
    expect(fuzzyScore('gpt-4o', 'o4g')).toBeNull()
    expect(fuzzyScore('gpt-4o', 'xyz')).toBeNull()
    expect(fuzzyScore('gpt-4o', 'gptx')).toBeNull()
  })

  it('returns matched positions into the original target', () => {
    const m = fuzzyScore('gpt-4o', 'g4o')
    // g@0, 4@4, o@5
    expect(m?.positions).toEqual([0, 4, 5])
  })

  it('treats an empty query as a zero-score match', () => {
    expect(fuzzyScore('anything', '')).toEqual({ score: 0, positions: [] })
  })

  it('scores an exact match highest', () => {
    const exact = fuzzyScore('sonnet', 'sonnet')!.score
    const prefix = fuzzyScore('sonnet-extended', 'sonnet')!.score
    // s,o,n,n,e,t all present in order but scattered across word boundaries.
    const scattered = fuzzyScore('snorkel-online-nnet', 'sonnet')!.score

    expect(exact).toBeGreaterThan(prefix)
    expect(prefix).toBeGreaterThan(scattered)
  })

  it('ranks a prefix match above a scattered subsequence', () => {
    const prefix = fuzzyScore('gpt-4o-mini', 'gpt')!.score
    const scattered = fuzzyScore('a-g-p-t', 'gpt')!.score

    expect(prefix).toBeGreaterThan(scattered)
  })

  it('rewards word-boundary matches', () => {
    // `s4` matching the `s` of sonnet and the `4` after a dash
    const boundary = fuzzyScore('claude-sonnet-4', 'cs4')
    expect(boundary).not.toBeNull()
  })
})

describe('fuzzyScoreMulti', () => {
  it('requires every space-separated token to match (AND)', () => {
    expect(fuzzyScoreMulti('claude-sonnet-4', 'clad snnt')).not.toBeNull()
    expect(fuzzyScoreMulti('claude-sonnet-4', 'claude haiku')).toBeNull()
  })

  it('unions matched positions across tokens, sorted', () => {
    const m = fuzzyScoreMulti('claude-sonnet', 'son cla')
    expect(m).not.toBeNull()
    expect(m!.positions).toEqual([...m!.positions].sort((a, b) => a - b))
  })

  it('treats whitespace-only query as a zero-score match', () => {
    expect(fuzzyScoreMulti('x', '   ')).toEqual({ score: 0, positions: [] })
  })
})

describe('fuzzyRank', () => {
  const models = ['gpt-4o', 'gpt-4o-mini', 'claude-sonnet-4', 'claude-haiku', 'o1-preview']

  it('drops non-matching items and ranks matches by score', () => {
    const ranked = fuzzyRank(models, 'g4o', m => m)
    const ids = ranked.map(r => r.item)

    expect(ids).toContain('gpt-4o')
    expect(ids).toContain('gpt-4o-mini')
    expect(ids).not.toContain('claude-haiku')
    // Shorter exact-ish prefix should outrank the longer variant.
    expect(ids.indexOf('gpt-4o')).toBeLessThan(ids.indexOf('gpt-4o-mini'))
  })

  it('ranks son4 so a sonnet model surfaces', () => {
    const ranked = fuzzyRank(models, 'son4', m => m)
    expect(ranked[0]?.item).toBe('claude-sonnet-4')
  })

  it('returns all items in original order for an empty query', () => {
    const ranked = fuzzyRank(models, '', m => m)
    expect(ranked.map(r => r.item)).toEqual(models)
    expect(ranked.every(r => r.positions.length === 0)).toBe(true)
  })

  it('is stable for equal scores (original index tiebreak)', () => {
    const items = ['ab', 'ab', 'ab']

    const ranked = fuzzyRank(
      items.map((v, i) => ({ v, i })),
      'ab',
      x => x.v
    )

    expect(ranked.map(r => r.item.i)).toEqual([0, 1, 2])
  })

  // Ids use hyphens, display names spaces, quants underscores, versions dots;
  // a user typing from memory picks any of them and must still get the row.
  it.each([
    ['gpt.4o', 'gpt-4o'],
    ['claude_3', 'claude-3-opus'],
    ['qwen3-8', 'qwen3.8-flash']
  ])('folds separators on both sides so %s finds %s', (query, expected) => {
    const catalog = ['gpt-4o', 'claude-3-opus', 'qwen3.8-flash', 'o1-preview']
    const ranked = fuzzyRank(catalog, query, m => m)

    expect(ranked[0]?.item).toBe(expected)
    // Positions still index the ORIGINAL target (fold is length-preserving).
    expect(ranked[0]!.positions.every(i => i >= 0 && i < expected.length)).toBe(true)
  })

  it('matches across a derived key, not just the raw string', () => {
    const providers = [
      { slug: 'openai', name: 'OpenAI' },
      { slug: 'anthropic', name: 'Anthropic' }
    ]

    const ranked = fuzzyRank(providers, 'anth', p => `${p.name} ${p.slug}`)
    expect(ranked[0]?.item.slug).toBe('anthropic')
  })
})