File size: 5,673 Bytes
39e315a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
import { describe, expect, test } from 'bun:test'

import {
  routeModel,
  type SmartRoutingConfig,
} from './smartModelRouting.ts'

const ENABLED: SmartRoutingConfig = {
  enabled: true,
  simpleModel: 'claude-haiku-4-5',
  strongModel: 'claude-opus-4-7',
}

describe('routeModel — disabled / misconfigured', () => {
  test('disabled config routes to strong', () => {
    const decision = routeModel(
      { userText: 'hi' },
      { ...ENABLED, enabled: false },
    )
    expect(decision.model).toBe('claude-opus-4-7')
    expect(decision.complexity).toBe('strong')
    expect(decision.reason).toContain('disabled')
  })

  test('missing simpleModel falls back to strong', () => {
    const decision = routeModel(
      { userText: 'hi' },
      { ...ENABLED, simpleModel: '' },
    )
    expect(decision.model).toBe('claude-opus-4-7')
    expect(decision.complexity).toBe('strong')
  })

  test('simpleModel === strongModel routes to strong (no-op)', () => {
    const decision = routeModel(
      { userText: 'hi' },
      { ...ENABLED, simpleModel: 'claude-opus-4-7' },
    )
    expect(decision.model).toBe('claude-opus-4-7')
    expect(decision.complexity).toBe('strong')
  })
})

describe('routeModel — simple path', () => {
  test('short greeting routes to simple', () => {
    const decision = routeModel({ userText: 'thanks!', turnNumber: 5 }, ENABLED)
    expect(decision.model).toBe('claude-haiku-4-5')
    expect(decision.complexity).toBe('simple')
  })

  test('empty input routes to simple', () => {
    const decision = routeModel({ userText: '   ' }, ENABLED)
    expect(decision.model).toBe('claude-haiku-4-5')
    expect(decision.complexity).toBe('simple')
  })

  test('mid-length chatter routes to simple', () => {
    const decision = routeModel(
      { userText: 'yep looks good, go ahead', turnNumber: 10 },
      ENABLED,
    )
    expect(decision.complexity).toBe('simple')
  })
})

describe('routeModel — strong path', () => {
  test('first turn always routes to strong, even when short', () => {
    const decision = routeModel(
      { userText: 'fix the bug', turnNumber: 1 },
      ENABLED,
    )
    expect(decision.model).toBe('claude-opus-4-7')
    expect(decision.complexity).toBe('strong')
    expect(decision.reason).toContain('first turn')
  })

  test('code fence routes to strong', () => {
    const decision = routeModel(
      {
        userText: 'change this:\n```\nfoo()\n```',
        turnNumber: 5,
      },
      ENABLED,
    )
    expect(decision.complexity).toBe('strong')
    expect(decision.reason).toContain('code')
  })

  test('inline code span routes to strong', () => {
    const decision = routeModel(
      { userText: 'rename `foo` to `bar`', turnNumber: 5 },
      ENABLED,
    )
    expect(decision.complexity).toBe('strong')
  })

  test('reasoning keyword "plan" routes to strong even when short', () => {
    const decision = routeModel(
      { userText: 'plan the refactor', turnNumber: 5 },
      ENABLED,
    )
    expect(decision.complexity).toBe('strong')
    expect(decision.reason).toContain('keyword')
  })

  test('reasoning keyword "debug" routes to strong', () => {
    const decision = routeModel(
      { userText: 'debug the test', turnNumber: 5 },
      ENABLED,
    )
    expect(decision.complexity).toBe('strong')
  })

  test('"root cause" multi-word keyword routes to strong', () => {
    const decision = routeModel(
      { userText: 'find the root cause', turnNumber: 5 },
      ENABLED,
    )
    expect(decision.complexity).toBe('strong')
  })

  test('multi-paragraph input routes to strong', () => {
    const decision = routeModel(
      {
        userText: 'first thought.\n\nsecond thought.',
        turnNumber: 5,
      },
      ENABLED,
    )
    expect(decision.complexity).toBe('strong')
    expect(decision.reason).toContain('multi-paragraph')
  })

  test('over-long input routes to strong', () => {
    const long = 'ok '.repeat(100) // ~300 chars, 100 words
    const decision = routeModel(
      { userText: long, turnNumber: 5 },
      ENABLED,
    )
    expect(decision.complexity).toBe('strong')
  })

  test('exactly at the boundary stays simple', () => {
    const text = 'a'.repeat(160)
    const decision = routeModel(
      { userText: text, turnNumber: 5 },
      { ...ENABLED, simpleMaxChars: 160, simpleMaxWords: 28 },
    )
    expect(decision.complexity).toBe('simple')
  })

  test('one char over the boundary routes to strong', () => {
    const text = 'a'.repeat(161)
    const decision = routeModel(
      { userText: text, turnNumber: 5 },
      { ...ENABLED, simpleMaxChars: 160, simpleMaxWords: 28 },
    )
    expect(decision.complexity).toBe('strong')
    expect(decision.reason).toContain('160 chars')
  })
})

describe('routeModel — config overrides', () => {
  test('custom simpleMaxChars is honored', () => {
    const decision = routeModel(
      { userText: 'abcdefghijklmnop', turnNumber: 5 },
      { ...ENABLED, simpleMaxChars: 10 },
    )
    expect(decision.complexity).toBe('strong')
    expect(decision.reason).toContain('10 chars')
  })

  test('custom simpleMaxWords is honored', () => {
    const decision = routeModel(
      { userText: 'one two three four five', turnNumber: 5 },
      { ...ENABLED, simpleMaxWords: 3 },
    )
    expect(decision.complexity).toBe('strong')
    expect(decision.reason).toContain('3 words')
  })
})

describe('routeModel — reason strings', () => {
  test('simple decisions include char + word counts', () => {
    const decision = routeModel(
      { userText: 'sounds good', turnNumber: 5 },
      ENABLED,
    )
    expect(decision.reason).toMatch(/\d+ chars, \d+ words/)
  })
})