W
File size: 44,923 Bytes
2b64d42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
/**
 * Model catalog β€” merged from hardcoded enum values + live GetCascadeModelConfigs.
 *
 * Routing logic:
 *   modelUid present  β†’ Cascade flow (StartCascade β†’ SendUserCascadeMessage)
 *   only enumValue>0  β†’ RawGetChatMessage (legacy)
 *
 * Credit multipliers sourced from GetCascadeModelConfigs (server.codeium.com).
 * Enum values sourced from Windsurf extension.js decompilation.
 */

export const MODELS = {
  // ── Claude ──────────────────────────────────────────────
  // Legacy 3.5 / 3.7 series β€” only have enumValue (legacy RawGetChatMessage flow), no modelUid.
  // Cascade upstream returns "neither PlanModel nor RequestedModel specified" for all three;
  // chat.js translates that to 410 model_deprecated when the catalog flag is set. issue #109.
  'claude-3.5-sonnet':              { name: 'claude-3.5-sonnet',              provider: 'anthropic', enumValue: 166, credit: 2, deprecated: true },
  'claude-3.7-sonnet':              { name: 'claude-3.7-sonnet',              provider: 'anthropic', enumValue: 226, credit: 2, deprecated: true },
  'claude-3.7-sonnet-thinking':     { name: 'claude-3.7-sonnet-thinking',     provider: 'anthropic', enumValue: 227, credit: 3, deprecated: true },
  'claude-4-sonnet':                { name: 'claude-4-sonnet',                provider: 'anthropic', enumValue: 281, modelUid: 'MODEL_CLAUDE_4_SONNET', credit: 2 },
  'claude-4-sonnet-thinking':       { name: 'claude-4-sonnet-thinking',       provider: 'anthropic', enumValue: 282, modelUid: 'MODEL_CLAUDE_4_SONNET_THINKING', credit: 3 },
  'claude-4-opus':                  { name: 'claude-4-opus',                  provider: 'anthropic', enumValue: 290, modelUid: 'MODEL_CLAUDE_4_OPUS', credit: 4 },
  'claude-4-opus-thinking':         { name: 'claude-4-opus-thinking',         provider: 'anthropic', enumValue: 291, modelUid: 'MODEL_CLAUDE_4_OPUS_THINKING', credit: 5 },
  'claude-4.1-opus':                { name: 'claude-4.1-opus',                provider: 'anthropic', enumValue: 328, modelUid: 'MODEL_CLAUDE_4_1_OPUS', credit: 4 },
  'claude-4.1-opus-thinking':       { name: 'claude-4.1-opus-thinking',       provider: 'anthropic', enumValue: 329, modelUid: 'MODEL_CLAUDE_4_1_OPUS_THINKING', credit: 5 },
  'claude-4.5-haiku':               { name: 'claude-4.5-haiku',               provider: 'anthropic', enumValue: 0,   modelUid: 'MODEL_PRIVATE_11', credit: 1 },
  'claude-4.5-sonnet':              { name: 'claude-4.5-sonnet',              provider: 'anthropic', enumValue: 353, modelUid: 'MODEL_PRIVATE_2', credit: 2 },
  'claude-4.5-sonnet-thinking':     { name: 'claude-4.5-sonnet-thinking',     provider: 'anthropic', enumValue: 354, modelUid: 'MODEL_PRIVATE_3', credit: 3 },
  'claude-4.5-opus':                { name: 'claude-4.5-opus',                provider: 'anthropic', enumValue: 391, modelUid: 'MODEL_CLAUDE_4_5_OPUS', credit: 4 },
  'claude-4.5-opus-thinking':       { name: 'claude-4.5-opus-thinking',       provider: 'anthropic', enumValue: 392, modelUid: 'MODEL_CLAUDE_4_5_OPUS_THINKING', credit: 5 },
  'claude-sonnet-4.6':              { name: 'claude-sonnet-4.6',              provider: 'anthropic', enumValue: 0,   modelUid: 'claude-sonnet-4-6', credit: 4 },
  'claude-sonnet-4.6-thinking':     { name: 'claude-sonnet-4.6-thinking',     provider: 'anthropic', enumValue: 0,   modelUid: 'claude-sonnet-4-6-thinking', credit: 6 },
  'claude-sonnet-4.6-1m':           { name: 'claude-sonnet-4.6-1m',           provider: 'anthropic', enumValue: 0,   modelUid: 'claude-sonnet-4-6-1m', credit: 12 },
  'claude-sonnet-4.6-thinking-1m':  { name: 'claude-sonnet-4.6-thinking-1m',  provider: 'anthropic', enumValue: 0,   modelUid: 'claude-sonnet-4-6-thinking-1m', credit: 16 },
  'claude-opus-4.6':                { name: 'claude-opus-4.6',                provider: 'anthropic', enumValue: 0,   modelUid: 'claude-opus-4-6', credit: 6 },
  'claude-opus-4.6-thinking':       { name: 'claude-opus-4.6-thinking',       provider: 'anthropic', enumValue: 0,   modelUid: 'claude-opus-4-6-thinking', credit: 8 },
  // Claude Opus 4.7 β€” Windsurf changelog 2026-04-16; new xhigh effort tier vs 4.6.
  // `medium` is the canonical default; low/high/xhigh/max are reasoning tiers,
  // each can be paired with -thinking for visible chain-of-thought.
  'claude-opus-4-7-medium':         { name: 'claude-opus-4-7-medium',         provider: 'anthropic', enumValue: 0,   modelUid: 'claude-opus-4-7-medium', credit: 8 },
  'claude-opus-4-7-low':            { name: 'claude-opus-4-7-low',            provider: 'anthropic', enumValue: 0,   modelUid: 'claude-opus-4-7-low', credit: 6 },
  'claude-opus-4-7-high':           { name: 'claude-opus-4-7-high',           provider: 'anthropic', enumValue: 0,   modelUid: 'claude-opus-4-7-high', credit: 10 },
  'claude-opus-4-7-xhigh':          { name: 'claude-opus-4-7-xhigh',          provider: 'anthropic', enumValue: 0,   modelUid: 'claude-opus-4-7-xhigh', credit: 12 },
  'claude-opus-4-7-medium-thinking': { name: 'claude-opus-4-7-medium-thinking', provider: 'anthropic', enumValue: 0, modelUid: 'claude-opus-4-7-medium-thinking', credit: 10 },
  'claude-opus-4-7-high-thinking':  { name: 'claude-opus-4-7-high-thinking',  provider: 'anthropic', enumValue: 0,   modelUid: 'claude-opus-4-7-high-thinking', credit: 12 },
  'claude-opus-4-7-xhigh-thinking': { name: 'claude-opus-4-7-xhigh-thinking', provider: 'anthropic', enumValue: 0,   modelUid: 'claude-opus-4-7-xhigh-thinking', credit: 16 },
  // `max` reasoning tier appeared in GetCascadeModelConfigs after the 4.7 launch β€” sits
  // above xhigh in the effort ladder. No -thinking sibling in cloud catalog yet.
  'claude-opus-4-7-max':            { name: 'claude-opus-4-7-max',            provider: 'anthropic', enumValue: 0,   modelUid: 'claude-opus-4-7-max', credit: 16 },

  // ── GPT ─────────────────────────────────────────────────
  'gpt-4o':                         { name: 'gpt-4o',                         provider: 'openai', enumValue: 109, modelUid: 'MODEL_CHAT_GPT_4O_2024_08_06', credit: 1 },
  'gpt-4o-mini':                    { name: 'gpt-4o-mini',                    provider: 'openai', enumValue: 113, credit: 0.5, deprecated: true },
  'gpt-4.1':                        { name: 'gpt-4.1',                        provider: 'openai', enumValue: 259, modelUid: 'MODEL_CHAT_GPT_4_1_2025_04_14', credit: 1 },
  'gpt-4.1-mini':                   { name: 'gpt-4.1-mini',                   provider: 'openai', enumValue: 260, credit: 0.5, deprecated: true },
  'gpt-4.1-nano':                   { name: 'gpt-4.1-nano',                   provider: 'openai', enumValue: 261, credit: 0.25, deprecated: true },
  'gpt-5':                          { name: 'gpt-5',                          provider: 'openai', enumValue: 340, modelUid: 'MODEL_PRIVATE_6', credit: 0.5 },
  'gpt-5-medium':                   { name: 'gpt-5-medium',                   provider: 'openai', enumValue: 0,   modelUid: 'MODEL_PRIVATE_7', credit: 1 },
  'gpt-5-high':                     { name: 'gpt-5-high',                     provider: 'openai', enumValue: 0,   modelUid: 'MODEL_PRIVATE_8', credit: 2 },
  'gpt-5-mini':                     { name: 'gpt-5-mini',                     provider: 'openai', enumValue: 337, credit: 0.25, deprecated: true },
  'gpt-5-codex':                    { name: 'gpt-5-codex',                    provider: 'openai', enumValue: 346, modelUid: 'MODEL_CHAT_GPT_5_CODEX', credit: 0.5 },

  // GPT-5.1
  'gpt-5.1':                        { name: 'gpt-5.1',                        provider: 'openai', enumValue: 0,   modelUid: 'MODEL_PRIVATE_12', credit: 0.5 },
  'gpt-5.1-low':                    { name: 'gpt-5.1-low',                    provider: 'openai', enumValue: 0,   modelUid: 'MODEL_PRIVATE_13', credit: 0.5 },
  'gpt-5.1-medium':                 { name: 'gpt-5.1-medium',                 provider: 'openai', enumValue: 0,   modelUid: 'MODEL_PRIVATE_14', credit: 1 },
  'gpt-5.1-high':                   { name: 'gpt-5.1-high',                   provider: 'openai', enumValue: 0,   modelUid: 'MODEL_PRIVATE_15', credit: 2 },
  'gpt-5.1-fast':                   { name: 'gpt-5.1-fast',                   provider: 'openai', enumValue: 0,   modelUid: 'MODEL_PRIVATE_20', credit: 1 },
  'gpt-5.1-low-fast':               { name: 'gpt-5.1-low-fast',               provider: 'openai', enumValue: 0,   modelUid: 'MODEL_PRIVATE_21', credit: 1 },
  'gpt-5.1-medium-fast':            { name: 'gpt-5.1-medium-fast',            provider: 'openai', enumValue: 0,   modelUid: 'MODEL_PRIVATE_22', credit: 2 },
  'gpt-5.1-high-fast':              { name: 'gpt-5.1-high-fast',              provider: 'openai', enumValue: 0,   modelUid: 'MODEL_PRIVATE_23', credit: 4 },

  // GPT-5.1 Codex
  'gpt-5.1-codex-low':              { name: 'gpt-5.1-codex-low',              provider: 'openai', enumValue: 0,   modelUid: 'MODEL_GPT_5_1_CODEX_LOW', credit: 0.5 },
  'gpt-5.1-codex-medium':           { name: 'gpt-5.1-codex-medium',           provider: 'openai', enumValue: 0,   modelUid: 'MODEL_PRIVATE_9', credit: 1 },
  'gpt-5.1-codex-mini-low':         { name: 'gpt-5.1-codex-mini-low',         provider: 'openai', enumValue: 0,   modelUid: 'MODEL_GPT_5_1_CODEX_MINI_LOW', credit: 0.25 },
  'gpt-5.1-codex-mini':             { name: 'gpt-5.1-codex-mini',             provider: 'openai', enumValue: 0,   modelUid: 'MODEL_PRIVATE_19', credit: 0.5 },
  'gpt-5.1-codex-max-low':          { name: 'gpt-5.1-codex-max-low',          provider: 'openai', enumValue: 0,   modelUid: 'MODEL_GPT_5_1_CODEX_MAX_LOW', credit: 1 },
  'gpt-5.1-codex-max-medium':       { name: 'gpt-5.1-codex-max-medium',       provider: 'openai', enumValue: 0,   modelUid: 'MODEL_GPT_5_1_CODEX_MAX_MEDIUM', credit: 1.25 },
  'gpt-5.1-codex-max-high':         { name: 'gpt-5.1-codex-max-high',         provider: 'openai', enumValue: 0,   modelUid: 'MODEL_GPT_5_1_CODEX_MAX_HIGH', credit: 1.5 },

  // GPT-5.2
  'gpt-5.2':                        { name: 'gpt-5.2',                        provider: 'openai', enumValue: 401, modelUid: 'MODEL_GPT_5_2_MEDIUM', credit: 2 },
  'gpt-5.2-none':                   { name: 'gpt-5.2-none',                   provider: 'openai', enumValue: 0,   modelUid: 'MODEL_GPT_5_2_NONE', credit: 1 },
  'gpt-5.2-low':                    { name: 'gpt-5.2-low',                    provider: 'openai', enumValue: 400, modelUid: 'MODEL_GPT_5_2_LOW', credit: 1 },
  'gpt-5.2-high':                   { name: 'gpt-5.2-high',                   provider: 'openai', enumValue: 402, modelUid: 'MODEL_GPT_5_2_HIGH', credit: 3 },
  'gpt-5.2-xhigh':                  { name: 'gpt-5.2-xhigh',                  provider: 'openai', enumValue: 403, modelUid: 'MODEL_GPT_5_2_XHIGH', credit: 8 },
  'gpt-5.2-none-fast':              { name: 'gpt-5.2-none-fast',              provider: 'openai', enumValue: 0,   modelUid: 'MODEL_GPT_5_2_NONE_PRIORITY', credit: 2 },
  'gpt-5.2-low-fast':               { name: 'gpt-5.2-low-fast',               provider: 'openai', enumValue: 0,   modelUid: 'MODEL_GPT_5_2_LOW_PRIORITY', credit: 2 },
  'gpt-5.2-medium-fast':            { name: 'gpt-5.2-medium-fast',            provider: 'openai', enumValue: 0,   modelUid: 'MODEL_GPT_5_2_MEDIUM_PRIORITY', credit: 4 },
  'gpt-5.2-high-fast':              { name: 'gpt-5.2-high-fast',              provider: 'openai', enumValue: 0,   modelUid: 'MODEL_GPT_5_2_HIGH_PRIORITY', credit: 6 },
  'gpt-5.2-xhigh-fast':             { name: 'gpt-5.2-xhigh-fast',             provider: 'openai', enumValue: 0,   modelUid: 'MODEL_GPT_5_2_XHIGH_PRIORITY', credit: 16 },

  // GPT-5.2 Codex
  'gpt-5.2-codex-low':              { name: 'gpt-5.2-codex-low',              provider: 'openai', enumValue: 0,   modelUid: 'MODEL_GPT_5_2_CODEX_LOW', credit: 1 },
  'gpt-5.2-codex-medium':           { name: 'gpt-5.2-codex-medium',           provider: 'openai', enumValue: 0,   modelUid: 'MODEL_GPT_5_2_CODEX_MEDIUM', credit: 1 },
  'gpt-5.2-codex-high':             { name: 'gpt-5.2-codex-high',             provider: 'openai', enumValue: 0,   modelUid: 'MODEL_GPT_5_2_CODEX_HIGH', credit: 2 },
  'gpt-5.2-codex-xhigh':            { name: 'gpt-5.2-codex-xhigh',            provider: 'openai', enumValue: 0,   modelUid: 'MODEL_GPT_5_2_CODEX_XHIGH', credit: 3 },
  'gpt-5.2-codex-low-fast':         { name: 'gpt-5.2-codex-low-fast',         provider: 'openai', enumValue: 0,   modelUid: 'MODEL_GPT_5_2_CODEX_LOW_PRIORITY', credit: 2 },
  'gpt-5.2-codex-medium-fast':      { name: 'gpt-5.2-codex-medium-fast',      provider: 'openai', enumValue: 0,   modelUid: 'MODEL_GPT_5_2_CODEX_MEDIUM_PRIORITY', credit: 2 },
  'gpt-5.2-codex-high-fast':        { name: 'gpt-5.2-codex-high-fast',        provider: 'openai', enumValue: 0,   modelUid: 'MODEL_GPT_5_2_CODEX_HIGH_PRIORITY', credit: 4 },
  'gpt-5.2-codex-xhigh-fast':       { name: 'gpt-5.2-codex-xhigh-fast',       provider: 'openai', enumValue: 0,   modelUid: 'MODEL_GPT_5_2_CODEX_XHIGH_PRIORITY', credit: 6 },

  // GPT-5.3 Codex (legacy key)
  'gpt-5.3-codex':                  { name: 'gpt-5.3-codex',                  provider: 'openai', enumValue: 0,   modelUid: 'gpt-5-3-codex-medium', credit: 1 },

  // GPT-5.4
  'gpt-5.4-none':                   { name: 'gpt-5.4-none',                   provider: 'openai', enumValue: 0,   modelUid: 'gpt-5-4-none', credit: 0.5 },
  'gpt-5.4-low':                    { name: 'gpt-5.4-low',                    provider: 'openai', enumValue: 0,   modelUid: 'gpt-5-4-low', credit: 1 },
  'gpt-5.4-medium':                 { name: 'gpt-5.4-medium',                 provider: 'openai', enumValue: 0,   modelUid: 'gpt-5-4-medium', credit: 2 },
  'gpt-5.4-high':                   { name: 'gpt-5.4-high',                   provider: 'openai', enumValue: 0,   modelUid: 'gpt-5-4-high', credit: 4 },
  'gpt-5.4-xhigh':                  { name: 'gpt-5.4-xhigh',                  provider: 'openai', enumValue: 0,   modelUid: 'gpt-5-4-xhigh', credit: 8 },
  'gpt-5.4-mini-low':               { name: 'gpt-5.4-mini-low',               provider: 'openai', enumValue: 0,   modelUid: 'gpt-5-4-mini-low', credit: 1.5 },
  'gpt-5.4-mini-medium':            { name: 'gpt-5.4-mini-medium',            provider: 'openai', enumValue: 0,   modelUid: 'gpt-5-4-mini-medium', credit: 1.5 },
  'gpt-5.4-mini-high':              { name: 'gpt-5.4-mini-high',              provider: 'openai', enumValue: 0,   modelUid: 'gpt-5-4-mini-high', credit: 4.5 },
  'gpt-5.4-mini-xhigh':             { name: 'gpt-5.4-mini-xhigh',             provider: 'openai', enumValue: 0,   modelUid: 'gpt-5-4-mini-xhigh', credit: 12 },

  // GPT-5.5 β€” Windsurf catalog 2026-04-30. Same effort ladder as 5.2/5.4 (none/low/medium/high/xhigh)
  // with priority (=fast) lane equivalents. Bare `gpt-5.5` defaults to medium.
  'gpt-5.5':                        { name: 'gpt-5.5',                        provider: 'openai', enumValue: 0,   modelUid: 'gpt-5-5-medium', credit: 2 },
  'gpt-5.5-none':                   { name: 'gpt-5.5-none',                   provider: 'openai', enumValue: 0,   modelUid: 'gpt-5-5-none', credit: 1 },
  'gpt-5.5-low':                    { name: 'gpt-5.5-low',                    provider: 'openai', enumValue: 0,   modelUid: 'gpt-5-5-low', credit: 1 },
  'gpt-5.5-medium':                 { name: 'gpt-5.5-medium',                 provider: 'openai', enumValue: 0,   modelUid: 'gpt-5-5-medium', credit: 2 },
  'gpt-5.5-high':                   { name: 'gpt-5.5-high',                   provider: 'openai', enumValue: 0,   modelUid: 'gpt-5-5-high', credit: 4 },
  'gpt-5.5-xhigh':                  { name: 'gpt-5.5-xhigh',                  provider: 'openai', enumValue: 0,   modelUid: 'gpt-5-5-xhigh', credit: 8 },
  'gpt-5.5-none-fast':              { name: 'gpt-5.5-none-fast',              provider: 'openai', enumValue: 0,   modelUid: 'gpt-5-5-none-priority', credit: 2 },
  'gpt-5.5-low-fast':               { name: 'gpt-5.5-low-fast',               provider: 'openai', enumValue: 0,   modelUid: 'gpt-5-5-low-priority', credit: 2 },
  'gpt-5.5-medium-fast':            { name: 'gpt-5.5-medium-fast',            provider: 'openai', enumValue: 0,   modelUid: 'gpt-5-5-medium-priority', credit: 4 },
  'gpt-5.5-high-fast':              { name: 'gpt-5.5-high-fast',              provider: 'openai', enumValue: 0,   modelUid: 'gpt-5-5-high-priority', credit: 8 },
  'gpt-5.5-xhigh-fast':             { name: 'gpt-5.5-xhigh-fast',             provider: 'openai', enumValue: 0,   modelUid: 'gpt-5-5-xhigh-priority', credit: 16 },

  // GPT-5.3 Codex β€” already had bare `gpt-5.3-codex` (legacy alias), now expose tier variants.
  'gpt-5.3-codex-low':              { name: 'gpt-5.3-codex-low',              provider: 'openai', enumValue: 0,   modelUid: 'gpt-5-3-codex-low', credit: 0.5 },
  'gpt-5.3-codex-high':             { name: 'gpt-5.3-codex-high',             provider: 'openai', enumValue: 0,   modelUid: 'gpt-5-3-codex-high', credit: 2 },
  'gpt-5.3-codex-xhigh':            { name: 'gpt-5.3-codex-xhigh',            provider: 'openai', enumValue: 0,   modelUid: 'gpt-5-3-codex-xhigh', credit: 4 },
  'gpt-5.3-codex-low-fast':         { name: 'gpt-5.3-codex-low-fast',         provider: 'openai', enumValue: 0,   modelUid: 'gpt-5-3-codex-low-priority', credit: 1 },
  'gpt-5.3-codex-medium-fast':      { name: 'gpt-5.3-codex-medium-fast',      provider: 'openai', enumValue: 0,   modelUid: 'gpt-5-3-codex-medium-priority', credit: 2 },
  'gpt-5.3-codex-high-fast':        { name: 'gpt-5.3-codex-high-fast',        provider: 'openai', enumValue: 0,   modelUid: 'gpt-5-3-codex-high-priority', credit: 4 },
  'gpt-5.3-codex-xhigh-fast':       { name: 'gpt-5.3-codex-xhigh-fast',       provider: 'openai', enumValue: 0,   modelUid: 'gpt-5-3-codex-xhigh-priority', credit: 6 },

  // GPT-OSS
  'gpt-oss-120b':                   { name: 'gpt-oss-120b',                   provider: 'openai', enumValue: 0,   modelUid: 'MODEL_GPT_OSS_120B', credit: 0.25 },

  // ── O-series ────────────────────────────────────────────
  'o3-mini':                        { name: 'o3-mini',                        provider: 'openai', enumValue: 207, credit: 0.5 },
  'o3':                             { name: 'o3',                             provider: 'openai', enumValue: 218, modelUid: 'MODEL_CHAT_O3', credit: 1 },
  'o3-high':                        { name: 'o3-high',                        provider: 'openai', enumValue: 0,   modelUid: 'MODEL_CHAT_O3_HIGH', credit: 1 },
  'o3-pro':                         { name: 'o3-pro',                         provider: 'openai', enumValue: 294, credit: 4 },
  'o4-mini':                        { name: 'o4-mini',                        provider: 'openai', enumValue: 264, credit: 0.5 },

  // ── Gemini ──────────────────────────────────────────────
  'gemini-2.5-pro':                 { name: 'gemini-2.5-pro',                 provider: 'google', enumValue: 246, modelUid: 'MODEL_GOOGLE_GEMINI_2_5_PRO', credit: 1 },
  'gemini-2.5-flash':               { name: 'gemini-2.5-flash',               provider: 'google', enumValue: 312, modelUid: 'MODEL_GOOGLE_GEMINI_2_5_FLASH', credit: 0.5 },
  'gemini-3.0-pro':                 { name: 'gemini-3.0-pro',                 provider: 'google', enumValue: 412, modelUid: 'MODEL_GOOGLE_GEMINI_3_0_PRO_LOW', credit: 1 },
  'gemini-3.0-flash-minimal':       { name: 'gemini-3.0-flash-minimal',       provider: 'google', enumValue: 0,   modelUid: 'MODEL_GOOGLE_GEMINI_3_0_FLASH_MINIMAL', credit: 0.75 },
  'gemini-3.0-flash-low':           { name: 'gemini-3.0-flash-low',           provider: 'google', enumValue: 0,   modelUid: 'MODEL_GOOGLE_GEMINI_3_0_FLASH_LOW', credit: 1 },
  'gemini-3.0-flash':               { name: 'gemini-3.0-flash',               provider: 'google', enumValue: 415, modelUid: 'MODEL_GOOGLE_GEMINI_3_0_FLASH_MEDIUM', credit: 1 },
  'gemini-3.0-flash-high':          { name: 'gemini-3.0-flash-high',          provider: 'google', enumValue: 0,   modelUid: 'MODEL_GOOGLE_GEMINI_3_0_FLASH_HIGH', credit: 1.75 },
  'gemini-3.1-pro-low':             { name: 'gemini-3.1-pro-low',             provider: 'google', enumValue: 0,   modelUid: 'gemini-3-1-pro-low', credit: 1 },
  'gemini-3.1-pro-high':            { name: 'gemini-3.1-pro-high',            provider: 'google', enumValue: 0,   modelUid: 'gemini-3-1-pro-high', credit: 2 },

  // ── DeepSeek ────────────────────────────────────────────
  'deepseek-v3':                    { name: 'deepseek-v3',                    provider: 'deepseek', enumValue: 205, credit: 0.5, deprecated: true },
  'deepseek-v3-2':                  { name: 'deepseek-v3-2',                  provider: 'deepseek', enumValue: 409, credit: 0.5, deprecated: true },
  'deepseek-r1':                    { name: 'deepseek-r1',                    provider: 'deepseek', enumValue: 206, credit: 1, deprecated: true },

  // ── Grok ────────────────────────────────────────────────
  'grok-3':                         { name: 'grok-3',                         provider: 'xai', enumValue: 217, modelUid: 'MODEL_XAI_GROK_3', credit: 1 },
  'grok-3-mini':                    { name: 'grok-3-mini',                    provider: 'xai', enumValue: 234, credit: 0.5, deprecated: true },
  'grok-3-mini-thinking':           { name: 'grok-3-mini-thinking',           provider: 'xai', enumValue: 0,   modelUid: 'MODEL_XAI_GROK_3_MINI_REASONING', credit: 0.125 },
  'grok-code-fast-1':               { name: 'grok-code-fast-1',               provider: 'xai', enumValue: 0,   modelUid: 'MODEL_PRIVATE_4', credit: 0.5 },

  // ── Qwen ────────────────────────────────────────────────
  'qwen-3':                         { name: 'qwen-3',                         provider: 'alibaba', enumValue: 324, credit: 0.5, deprecated: true },
  // qwen-3-coder + qwen-3-coder-fast: exist in binary enum (325/327)
  // but cascade server doesn't have any routing registered for them β€”
  // both enum-only and explicit UIDs fail with 'model not found'.
  // Removed from catalog until upstream registers them.

  // ── Kimi ────────────────────────────────────────────────
  'kimi-k2':                        { name: 'kimi-k2',                        provider: 'moonshot', enumValue: 323, modelUid: 'MODEL_KIMI_K2', credit: 0.5 },
  'kimi-k2-thinking':               { name: 'kimi-k2-thinking',               provider: 'moonshot', enumValue: 394, modelUid: 'MODEL_KIMI_K2_THINKING', credit: 1 },
  'kimi-k2.5':                      { name: 'kimi-k2.5',                      provider: 'moonshot', enumValue: 0,   modelUid: 'kimi-k2-5', credit: 1 },
  'kimi-k2-6':                      { name: 'kimi-k2-6',                      provider: 'moonshot', enumValue: 0,   modelUid: 'kimi-k2-6', credit: 1 },

  // ── GLM ─────────────────────────────────────────────────
  'glm-4.7':                        { name: 'glm-4.7',                        provider: 'zhipu', enumValue: 417, modelUid: 'MODEL_GLM_4_7', credit: 0.25 },
  'glm-4.7-fast':                   { name: 'glm-4.7-fast',                   provider: 'zhipu', enumValue: 418, modelUid: 'MODEL_GLM_4_7_FAST', credit: 0.5 },
  'glm-5':                          { name: 'glm-5',                          provider: 'zhipu', enumValue: 0,   modelUid: 'glm-5', credit: 1.5 },
  'glm-5.1':                        { name: 'glm-5.1',                        provider: 'zhipu', enumValue: 0,   modelUid: 'glm-5-1', credit: 1.5 },

  // ── MiniMax ─────────────────────────────────────────────
  // proto enum 419 = MODEL_MINIMAX_M2_1; the canonical name in cloud configs is m2.5.
  'minimax-m2.5':                   { name: 'minimax-m2.5',                   provider: 'minimax', enumValue: 419, modelUid: 'MODEL_MINIMAX_M2_1', credit: 1 },

  // ── Windsurf SWE ────────────────────────────────────────
  // Proto canonical enums: 359=MODEL_SWE_1_5 (fast), 369=THINKING, 377=SLOW, 420=1_6, 421=1_6_FAST.
  // The default `swe-1.5` UID alias in upstream cloud config maps to the SLOW tier (377).
  'swe-1.5':                        { name: 'swe-1.5',                        provider: 'windsurf', enumValue: 377, modelUid: 'MODEL_SWE_1_5_SLOW', credit: 0.5 },
  'swe-1.5-fast':                   { name: 'swe-1.5-fast',                   provider: 'windsurf', enumValue: 359, modelUid: 'MODEL_SWE_1_5', credit: 0.5 },
  'swe-1.5-thinking':               { name: 'swe-1.5-thinking',               provider: 'windsurf', enumValue: 369, modelUid: 'MODEL_SWE_1_5_THINKING', credit: 0.75 },
  'swe-1.6':                        { name: 'swe-1.6',                        provider: 'windsurf', enumValue: 420, modelUid: 'MODEL_SWE_1_6', credit: 0.5 },
  'swe-1.6-fast':                   { name: 'swe-1.6-fast',                   provider: 'windsurf', enumValue: 421, modelUid: 'MODEL_SWE_1_6_FAST', credit: 0.5 },

  // ── Adaptive (Windsurf 2026-04-06 changelog) ────────────
  // Adaptive Model Router + Arena models live in the cloud catalog but their
  // UIDs aren't recognized by SendUserCascadeMessage's direct-call path β€”
  // upstream returns "unknown model UID adaptive: model not found". They only
  // work through the Windsurf IDE's special routing layer that Cascade-direct
  // doesn't expose. Mark deprecated so they stop showing in /v1/models. #109.
  'adaptive':                       { name: 'adaptive',                       provider: 'windsurf', enumValue: 0,   modelUid: 'adaptive', credit: 1, deprecated: true },
  'arena-fast':                     { name: 'arena-fast',                     provider: 'windsurf', enumValue: 0,   modelUid: 'arena-fast', credit: 0.5, deprecated: true },
  'arena-smart':                    { name: 'arena-smart',                    provider: 'windsurf', enumValue: 0,   modelUid: 'arena-smart', credit: 1, deprecated: true },
};

// Build reverse lookup
const _lookup = new Map();
for (const [id, info] of Object.entries(MODELS)) {
  _lookup.set(id, id);
  _lookup.set(id.toLowerCase(), id);
  _lookup.set(info.name, id);
  _lookup.set(info.name.toLowerCase(), id);
  if (info.modelUid) _lookup.set(info.modelUid, id);
  if (info.modelUid) _lookup.set(info.modelUid.toLowerCase(), id);
}
// Legacy aliases
_lookup.set('claude-sonnet-4-6-thinking', 'claude-sonnet-4.6-thinking');
_lookup.set('claude-opus-4-6-thinking', 'claude-opus-4.6-thinking');
_lookup.set('claude-sonnet-4-6', 'claude-sonnet-4.6');
_lookup.set('claude-opus-4-6', 'claude-opus-4.6');
_lookup.set('MODEL_CLAUDE_4_5_SONNET', 'claude-4.5-sonnet');
_lookup.set('MODEL_CLAUDE_4_5_SONNET_THINKING', 'claude-4.5-sonnet-thinking');
// UID-based aliases not already covered by modelUid field
_lookup.set('claude-sonnet-4-6-1m', 'claude-sonnet-4.6-1m');
_lookup.set('claude-sonnet-4-6-thinking-1m', 'claude-sonnet-4.6-thinking-1m');
// Bare `claude-4.6` (no explicit sonnet/opus) β€” issue #68. Without these,
// resolveModel falls through to the raw string, getModelInfo returns null,
// and chat.js silently routes to legacy rawGetChatMessage with no model
// name, so the upstream falls back to a default model whose self-knowledge
// is "I'm Claude 4.5". Default the bare alias to sonnet (more common).
_lookup.set('claude-4.6', 'claude-sonnet-4.6');
_lookup.set('claude-4.6-thinking', 'claude-sonnet-4.6-thinking');
_lookup.set('claude-4.6-1m', 'claude-sonnet-4.6-1m');
_lookup.set('claude-4.6-thinking-1m', 'claude-sonnet-4.6-thinking-1m');
_lookup.set('gpt-5-4-none', 'gpt-5.4-none');
_lookup.set('gpt-5-4-low', 'gpt-5.4-low');
_lookup.set('gpt-5-4-medium', 'gpt-5.4-medium');
_lookup.set('gpt-5-4-high', 'gpt-5.4-high');
_lookup.set('gpt-5-4-xhigh', 'gpt-5.4-xhigh');
_lookup.set('gpt-5-4-mini-low', 'gpt-5.4-mini-low');
_lookup.set('gpt-5-4-mini-medium', 'gpt-5.4-mini-medium');
_lookup.set('gpt-5-4-mini-high', 'gpt-5.4-mini-high');
_lookup.set('gpt-5-4-mini-xhigh', 'gpt-5.4-mini-xhigh');
// Bare-tier aliases β€” clients commonly write the dotted form for the medium tier
// even when the catalog uses bare-only or tier-only entries. Without these the
// /v1/messages handler 400s "Unsupported model" before forwarding. #109 sub2api
// reproducer was `gpt-5.2-medium` (bare gpt-5.2 = medium but the alias was missing).
_lookup.set('gpt-5.2-medium', 'gpt-5.2');                  // bare gpt-5.2 IS the medium tier
_lookup.set('gpt-5-2-medium', 'gpt-5.2');                  // cloud-format equivalent
_lookup.set('gpt-5.2-codex', 'gpt-5.2-codex-medium');      // bare codex β†’ medium
_lookup.set('gpt-5-2-codex-medium', 'gpt-5.2-codex-medium');
_lookup.set('gpt-5.3-codex-medium', 'gpt-5.3-codex');      // bare codex IS medium
_lookup.set('gpt-5.4', 'gpt-5.4-medium');                  // bare β†’ medium per family convention
// gpt-5.5 cloud-format aliases (cloud sends `gpt-5-5-*`, OpenAI-style is `gpt-5.5-*`)
_lookup.set('gpt-5-5', 'gpt-5.5');
_lookup.set('gpt-5-5-none', 'gpt-5.5-none');
_lookup.set('gpt-5-5-low', 'gpt-5.5-low');
_lookup.set('gpt-5-5-medium', 'gpt-5.5-medium');
_lookup.set('gpt-5-5-high', 'gpt-5.5-high');
_lookup.set('gpt-5-5-xhigh', 'gpt-5.5-xhigh');
_lookup.set('gpt-5-5-none-priority', 'gpt-5.5-none-fast');
_lookup.set('gpt-5-5-low-priority', 'gpt-5.5-low-fast');
_lookup.set('gpt-5-5-medium-priority', 'gpt-5.5-medium-fast');
_lookup.set('gpt-5-5-high-priority', 'gpt-5.5-high-fast');
_lookup.set('gpt-5-5-xhigh-priority', 'gpt-5.5-xhigh-fast');
// gpt-5.3-codex tier aliases
_lookup.set('gpt-5-3-codex-low', 'gpt-5.3-codex-low');
_lookup.set('gpt-5-3-codex-medium', 'gpt-5.3-codex');
_lookup.set('gpt-5-3-codex-high', 'gpt-5.3-codex-high');
_lookup.set('gpt-5-3-codex-xhigh', 'gpt-5.3-codex-xhigh');
_lookup.set('gpt-5-3-codex-low-priority', 'gpt-5.3-codex-low-fast');
_lookup.set('gpt-5-3-codex-medium-priority', 'gpt-5.3-codex-medium-fast');
_lookup.set('gpt-5-3-codex-high-priority', 'gpt-5.3-codex-high-fast');
_lookup.set('gpt-5-3-codex-xhigh-priority', 'gpt-5.3-codex-xhigh-fast');
// Cloud-format aliases for existing dotted names
_lookup.set('swe-1-6', 'swe-1.6');
_lookup.set('swe-1-6-fast', 'swe-1.6-fast');
_lookup.set('minimax-m2-5', 'minimax-m2.5');
_lookup.set('kimi-k2-5', 'kimi-k2.5');

// Anthropic official dated names β€” Cursor / Claude Code / Anthropic SDK
// all send these verbatim. Map each to our short key so the same client
// can talk to this API without a custom-name translation layer.
const ANTHROPIC_DATED = {
  'claude-3-5-sonnet-20240620': 'claude-3.5-sonnet',
  'claude-3-5-sonnet-20241022': 'claude-3.5-sonnet',
  'claude-3-5-sonnet-latest':   'claude-3.5-sonnet',
  'claude-3-7-sonnet-20250219': 'claude-3.7-sonnet',
  'claude-3-7-sonnet-latest':   'claude-3.7-sonnet',
  'claude-sonnet-4-20250514':   'claude-4-sonnet',
  'claude-sonnet-4-0':          'claude-4-sonnet',
  'claude-opus-4-20250514':     'claude-4-opus',
  'claude-opus-4-0':            'claude-4-opus',
  'claude-opus-4-1':            'claude-4.1-opus',
  'claude-opus-4-1-20250805':   'claude-4.1-opus',
  'claude-sonnet-4-5':          'claude-4.5-sonnet',
  'claude-sonnet-4-5-20250929': 'claude-4.5-sonnet',
  'claude-sonnet-4-5-latest':   'claude-4.5-sonnet',
  'claude-opus-4-5':            'claude-4.5-opus',
  'claude-opus-4-5-20251101':   'claude-4.5-opus',
  'claude-opus-4-5-latest':     'claude-4.5-opus',
  // Claude Haiku 4.5 β€” Anthropic official id `claude-haiku-4-5-20251001`
  // (#117 xiaoxin-zk: dashboard test sent the dated form, hit
  // "Unsupported model" 400 because no alias existed). Cover the dated
  // name + bare + latest the same way sonnet/opus already are.
  'claude-haiku-4-5':           'claude-4.5-haiku',
  'claude-haiku-4-5-20251001':  'claude-4.5-haiku',
  'claude-haiku-4-5-latest':    'claude-4.5-haiku',
  // v2.0.85: README + every recent reply uses the dotted form
  // `claude-haiku-4.5` (mirrors `claude-sonnet-4.6`). Alias both
  // dotted and dashed so users following the docs verbatim don't hit
  // 400 model_not_found.
  'claude-haiku-4.5':           'claude-4.5-haiku',
  'claude-haiku-4.5-latest':    'claude-4.5-haiku',
  // Sonnet 4.5 dotted-suffix variants for the same reason.
  'claude-sonnet-4.5':          'claude-4.5-sonnet',
  'claude-sonnet-4.5-thinking': 'claude-4.5-sonnet-thinking',
  'claude-opus-4.5':            'claude-4.5-opus',
  'claude-opus-4.5-thinking':   'claude-4.5-opus-thinking',
  // Legacy Haiku dated names β€” Anthropic SDK clients sometimes still
  // ship these. Map to the closest live model (4.5-haiku) so the request
  // doesn't 400; the `deprecated` flag isn't set on 4.5-haiku so it
  // routes normally.
  'claude-3-5-haiku-20241022':  'claude-4.5-haiku',
  'claude-3-5-haiku-latest':    'claude-4.5-haiku',
  'claude-haiku-3-5':           'claude-4.5-haiku',
  'claude-haiku-3-5-latest':    'claude-4.5-haiku',

  // Anthropic Opus 4.7 β€” Windsurf changelog 2026-04-16. Cloud now exposes 4 reasoning
  // tiers (low/medium/high/xhigh) plus matching -thinking variants. Bare `claude-opus-4-7`
  // and `claude-opus-4.7` default to medium; `-thinking` suffix routes to medium-thinking.
  'claude-opus-4-7':            'claude-opus-4-7-medium',
  'claude-opus-4-7-latest':     'claude-opus-4-7-medium',
  'claude-opus-4.7':            'claude-opus-4-7-medium',
  'claude-opus-4.7-thinking':   'claude-opus-4-7-medium-thinking',
  'claude-opus-4-7-thinking':   'claude-opus-4-7-medium-thinking',
  'claude-opus-4.7-low':        'claude-opus-4-7-low',
  'claude-opus-4.7-medium':     'claude-opus-4-7-medium',
  'claude-opus-4.7-high':       'claude-opus-4-7-high',
  'claude-opus-4.7-xhigh':      'claude-opus-4-7-xhigh',
  'claude-opus-4.7-medium-thinking': 'claude-opus-4-7-medium-thinking',
  'claude-opus-4.7-high-thinking':   'claude-opus-4-7-high-thinking',
  'claude-opus-4.7-xhigh-thinking':  'claude-opus-4-7-xhigh-thinking',
  'claude-opus-4.7-max':             'claude-opus-4-7-max',
};
for (const [k, v] of Object.entries(ANTHROPIC_DATED)) _lookup.set(k, v);

// OpenAI official dated names β€” same pattern
const OPENAI_DATED = {
  'gpt-4o-2024-11-20': 'gpt-4o',
  'gpt-4o-2024-08-06': 'gpt-4o',
  'gpt-4o-2024-05-13': 'gpt-4o',
  'gpt-4o-mini-2024-07-18': 'gpt-4o-mini',
  'gpt-4.1-2025-04-14': 'gpt-4.1',
  'gpt-4.1-mini-2025-04-14': 'gpt-4.1-mini',
  'gpt-4.1-nano-2025-04-14': 'gpt-4.1-nano',
  'gpt-5-2025-08-07': 'gpt-5',
  'gpt-5-pro-2025-10-06': 'gpt-5-high',
  // GPT-5.5 β€” bare aliases default to medium tier (matches gpt-5.2 / gpt-5.4 pattern).
  'gpt-5-5':    'gpt-5.5-medium',
  'gpt-5.5':    'gpt-5.5-medium',
};
for (const [k, v] of Object.entries(OPENAI_DATED)) _lookup.set(k, v);

// Cursor-friendly aliases β€” Cursor's client-side whitelist blocks model names
// containing "claude". These prefixes bypass the filter while resolving to the
// same Windsurf backend models. Use any of these in Cursor's Custom Model field.
const CURSOR_ALIASES = {
  // opus
  'opus-4.6':              'claude-opus-4.6',
  'opus-4.6-thinking':     'claude-opus-4.6-thinking',
  'opus-4.7-thinking':     'claude-opus-4-7-medium-thinking',
  'opus-4-7':              'claude-opus-4-7-medium',
  'opus-4.7':              'claude-opus-4-7-medium',
  'o4.7':                  'claude-opus-4-7-medium',
  // sonnet
  'sonnet-4.6':            'claude-sonnet-4.6',
  'sonnet-4.6-thinking':   'claude-sonnet-4.6-thinking',
  'sonnet-4.6-1m':         'claude-sonnet-4.6-1m',
  'sonnet-4.5':            'claude-4.5-sonnet',
  'sonnet-4.5-thinking':   'claude-4.5-sonnet-thinking',
  // haiku
  'haiku-4.5':             'claude-4.5-haiku',
  // older
  'sonnet-4':              'claude-4-sonnet',
  'opus-4':                'claude-4-opus',
  'opus-4.1':              'claude-4.1-opus',
  'sonnet-3.7':            'claude-3.7-sonnet',
  'sonnet-3.5':            'claude-3.5-sonnet',
  // ws-* prefix variant (even safer against future whitelist updates)
  'ws-opus':               'claude-opus-4.6',
  'ws-sonnet':             'claude-sonnet-4.6',
  'ws-opus-thinking':      'claude-opus-4.6-thinking',
  'ws-sonnet-thinking':    'claude-sonnet-4.6-thinking',
  'ws-haiku':              'claude-4.5-haiku',
};
for (const [k, v] of Object.entries(CURSOR_ALIASES)) _lookup.set(k, v);

/** Resolve user model name β†’ internal model key. */
export function resolveModel(name) {
  if (!name) return null;
  return _lookup.get(name) || _lookup.get(name.toLowerCase()) || name;
}

/** Get model info including enum and uid. */
export function getModelInfo(id) {
  return MODELS[id] || null;
}

// v2.0.84 (#118 0a00) β€” when an entire account pool is rate-limited
// on a high-effort variant (`-max` / `-xhigh` / `-thinking-1m`), find
// a same-base lower-effort variant the user could fall back to. Used
// for two purposes:
//   1. Error remediation: include the suggested model in the 429
//      response so the client can switch transparently.
//   2. Optional auto-fallback (env opt-in): proxy retries the same
//      request against the lower variant before reporting failure.
//
// Returns null when no lower variant exists in the catalog. Effort
// ladder is suffix-only β€” we don't infer ladders, we read them off
// the literal model-key suffix.
//
// Suffix order: less expensive first β†’ more expensive last.
const EFFORT_LADDER = [
  // Anthropic effort tiers
  'low', 'medium', 'high', 'xhigh', 'max',
  // GPT codex max sub-tiers (claude has -low, -medium, -high; gpt
  // codex has -low / -medium / -high stacked under -max-)
];
const CONTEXT_LADDER = ['1m']; // 1m context variants are weekly-quota'd

// v2.0.89 (audit follow-up to v2.0.88 H-1.5): cascade pool alias
// fingerprint relies on `toolPreamble` being IDENTICAL between the
// stored fpAfterAlias and the next-turn fpBefore. toolPreamble depends
// on the dialect picked for (modelKey, provider, route). Inside one
// provider the dialect normally stays the same, so the alias slot
// fingerprint matches the next-turn lookup. But a cross-provider
// fallback (e.g. anthropic claude-opus β†’ openai gpt-5.5) would build
// the alias slot with the gpt_native dialect's toolPreamble while the
// next turn rebuilds with claude's dialect β†’ silent fingerprint
// mismatch β†’ cascade reuse miss β†’ model "forgets" prior turns again,
// regressing the v2.0.87 fix that the v2.0.88 alias write was meant
// to enforce.
//
// Today the EFFORT_LADDER and CONTEXT_LADDER walk only ever stays
// inside the same base model name (claude-opus-4-7-* siblings are all
// anthropic; codex max-* are all openai). But this is fragile β€”
// future catalog edits could produce a cross-provider candidate by
// accident. Add a hard guard: only return a fallback that has the
// same `provider` as the original.
function _isSameProviderFallback(originalKey, candidateKey) {
  const o = MODELS[originalKey];
  const c = MODELS[candidateKey];
  if (!o || !c) return false;
  // No provider on either side β†’ conservatively allow (matches old
  // behaviour for entries that haven't been catalogued with provider
  // metadata, though all current entries do have provider).
  if (!o.provider || !c.provider) return true;
  return o.provider === c.provider;
}

export function pickRateLimitFallback(modelKey) {
  if (!modelKey || typeof modelKey !== 'string') return null;
  // Try effort suffix first (e.g. -max β†’ -xhigh β†’ -high β†’ -medium β†’ -low)
  for (let i = EFFORT_LADDER.length - 1; i >= 1; i--) {
    const suffix = `-${EFFORT_LADDER[i]}`;
    if (modelKey.endsWith(suffix)) {
      const base = modelKey.slice(0, -suffix.length);
      // Walk DOWN the ladder until we find a key actually in the catalog
      // AND from the same provider (cascade pool alias requires same
      // dialect β†’ same toolPreamble β†’ same fingerprint).
      for (let j = i - 1; j >= 0; j--) {
        const candidate = `${base}-${EFFORT_LADDER[j]}`;
        if (MODELS[candidate] && _isSameProviderFallback(modelKey, candidate)) return candidate;
      }
    }
  }
  // 1m context variants β†’ drop -1m
  for (const suffix of CONTEXT_LADDER) {
    const dashed = `-${suffix}`;
    if (modelKey.endsWith(dashed)) {
      const candidate = modelKey.slice(0, -dashed.length);
      if (MODELS[candidate] && _isSameProviderFallback(modelKey, candidate)) return candidate;
    }
  }
  // -thinking variants don't have a simple ladder; the natural fallback
  // is the non-thinking sibling, but that changes user-visible behaviour
  // (no reasoning content). Skip auto-fallback for those.
  return null;
}

// Reverse map: Model enum number β†’ list of catalog keys (enum may match
// multiple variants if we ever dupe, but typically 1:1).
const _enumToKeys = (() => {
  const m = new Map();
  for (const [key, info] of Object.entries(MODELS)) {
    if (info.enumValue && info.enumValue > 0) {
      const arr = m.get(info.enumValue) || [];
      arr.push(key);
      m.set(info.enumValue, arr);
    }
  }
  return m;
})();

/** Reverse-lookup a Model enum number to our catalog keys. */
export function getModelKeysByEnum(enumValue) {
  return _enumToKeys.get(enumValue) || [];
}

// ─── Tier access ───────────────────────────────────────────

const FREE_TIER_BASE = ['gemini-2.5-flash'];
const _discoveredFreeModels = new Set();

export function registerDiscoveredFreeModel(key) {
  if (MODELS[key] && !FREE_TIER_BASE.includes(key)) _discoveredFreeModels.add(key);
}

export const MODEL_TIER_ACCESS = {
  get pro() { return Object.keys(MODELS); },
  get free() { return [...FREE_TIER_BASE, ..._discoveredFreeModels]; },
  // Optimistic: a freshly-added account whose probe hasn't completed yet
  // gets the FULL pro catalog, not just gemini-2.5-flash. Otherwise the
  // chat.js anyEligible check (line ~1141) immediately 403s any non-free
  // model with "ζ¨‘εž‹ X εœ¨ε½“ε‰θ΄¦ε·ζ± δΈ­δΈε―η”¨", and users see "添加账号后
  // δΈθƒ½θ°ƒη”¨δ»»δ½•ζ¨‘εž‹" until probe finishes ~10-30s later. Trade-off: a
  // free user may try opus before probe completes; the request will fail
  // upstream with a real entitlement error from the LS, which is a more
  // accurate failure than the misleading "model not in account pool" we
  // were emitting. Reported in QQ group, 2026-04-30.
  get unknown() { return Object.keys(MODELS); },
  expired: [],
};

/** Models a given tier is entitled to. */
export function getTierModels(tier) {
  return MODEL_TIER_ACCESS[tier] || MODEL_TIER_ACCESS.unknown;
}

/** List all models in OpenAI /v1/models format. Hides deprecated models. */
export function listModels() {
  const ts = Math.floor(Date.now() / 1000);
  return Object.entries(MODELS)
    .filter(([, info]) => !info.deprecated)
    .map(([id, info]) => ({
      id: info.name,
      object: 'model',
      created: ts,
      owned_by: info.provider,
      _windsurf_id: id,
    }));
}

/**
 * Merge live model configs from GetCascadeModelConfigs into the catalog.
 * Called once at startup after the first successful cloud fetch.
 * Only adds NEW models not already in the catalog (doesn't overwrite enums).
 */
export function mergeCloudModels(configs) {
  if (!Array.isArray(configs)) return 0;
  let added = 0;
  const providerMap = {
    MODEL_PROVIDER_ANTHROPIC: 'anthropic',
    MODEL_PROVIDER_OPENAI: 'openai',
    MODEL_PROVIDER_GOOGLE: 'google',
    MODEL_PROVIDER_DEEPSEEK: 'deepseek',
    MODEL_PROVIDER_XAI: 'xai',
    MODEL_PROVIDER_WINDSURF: 'windsurf',
    MODEL_PROVIDER_MOONSHOT: 'moonshot',
  };

  for (const m of configs) {
    const uid = m.modelUid;
    if (!uid) continue;
    // Already in catalog?
    if (_lookup.has(uid) || _lookup.has(uid.toLowerCase())) continue;

    const key = uid.toLowerCase().replace(/_/g, '-');
    if (MODELS[key]) continue;

    const provider = providerMap[m.provider] || m.provider?.toLowerCase()?.replace('model_provider_', '') || 'unknown';
    MODELS[key] = {
      name: key,
      provider,
      enumValue: 0,
      modelUid: uid,
      credit: m.creditMultiplier || 1,
    };
    _lookup.set(key, key);
    _lookup.set(uid, key);
    _lookup.set(uid.toLowerCase(), key);
    added++;
  }
  return added;
}