W
File size: 5,978 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
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { resolveModel, getModelInfo, getModelKeysByEnum, MODEL_TIER_ACCESS } from '../src/models.js';

describe('resolveModel', () => {
  it('resolves exact model names', () => {
    assert.equal(resolveModel('gpt-4o'), 'gpt-4o');
  });

  it('resolves case-insensitive aliases', () => {
    assert.equal(resolveModel('GPT-4O'), 'gpt-4o');
  });

  it('resolves Anthropic dated aliases', () => {
    const result = resolveModel('claude-3-5-sonnet-20240620');
    assert.equal(result, 'claude-3.5-sonnet');
  });

  it('resolves Cursor-friendly aliases without claude prefix', () => {
    const result = resolveModel('opus-4.6');
    assert.equal(result, 'claude-opus-4.6');
  });

  // Issue #68 — bare `claude-4.6` (no sonnet/opus split) used to fall through
  // to silent legacy fallback; the model would self-identify as "Claude 4.5"
  // because no model name was forwarded upstream. Default to sonnet.
  it('resolves bare claude-4.6 to sonnet variant', () => {
    assert.equal(resolveModel('claude-4.6'), 'claude-sonnet-4.6');
    assert.equal(resolveModel('claude-4.6-thinking'), 'claude-sonnet-4.6-thinking');
    assert.equal(resolveModel('claude-4.6-1m'), 'claude-sonnet-4.6-1m');
    assert.equal(resolveModel('claude-4.6-thinking-1m'), 'claude-sonnet-4.6-thinking-1m');
  });

  it('bare claude-4.6 resolves to a real catalog entry (not silent fallback)', () => {
    const info = getModelInfo(resolveModel('claude-4.6'));
    assert.ok(info, 'claude-4.6 must map to a known model');
    assert.equal(info.modelUid, 'claude-sonnet-4-6');
  });

  it('returns input unchanged for unknown models', () => {
    assert.equal(resolveModel('nonexistent-model-xyz'), 'nonexistent-model-xyz');
  });

  it('returns null for null/empty input', () => {
    assert.equal(resolveModel(null), null);
    assert.equal(resolveModel(''), null);
  });
});

describe('resolveModel Opus 4.7 / legacy alias coverage', () => {
  it('resolves Opus 4.7 aliases to canonical catalog keys', () => {
    assert.equal(resolveModel('claude-opus-4.7'), 'claude-opus-4-7-medium');
    assert.equal(resolveModel('claude-opus-4.7-thinking'), 'claude-opus-4-7-medium-thinking');
    assert.equal(resolveModel('claude-opus-4.7-high-thinking'), 'claude-opus-4-7-high-thinking');
    assert.equal(resolveModel('claude-Opus-4.7'), 'claude-opus-4-7-medium');
    assert.equal(resolveModel('CLAUDE-OPUS-4.7'), 'claude-opus-4-7-medium');
    assert.equal(resolveModel('claude.opus.4.7'), 'claude.opus.4.7');
  });

  it('documents unsupported bare / separator variants explicitly', () => {
    // Underscores aren't a recognized separator
    assert.equal(resolveModel('claude_opus_4_7'), 'claude_opus_4_7');
    // Bare `opus-4.7-xhigh` (no `claude-` prefix on a tier-suffix variant) — not aliased
    assert.equal(resolveModel('opus-4.7-xhigh'), 'opus-4.7-xhigh');
    // Too bare to disambiguate (sonnet vs opus)
    assert.equal(resolveModel('4.7-medium'), '4.7-medium');
    // Note: `opus-4.7-thinking` IS now supported (added by spark-C audit alongside `o4.7`).
    // See test/models-catalog-correctness.test.js for the positive assertion.
  });
});

describe('reverse-lookup model info coverage', () => {
  it('resolves kimi-k2-thinking, glm-4.7-fast, and adaptive', () => {
    const modelKeys = ['kimi-k2-thinking', 'glm-4.7-fast', 'adaptive'];
    for (const raw of modelKeys) {
      const resolved = resolveModel(raw);
      const info = getModelInfo(resolved);
      assert.ok(info, `missing model info for ${raw}`);
      assert.equal(info.name, resolved, `info.name mismatch for ${raw}`);
    }
  });
});

describe('getModelInfo', () => {
  it('returns model info for known model', () => {
    const info = getModelInfo('gpt-4o');
    assert.ok(info);
    assert.ok(info.enumValue > 0 || info.modelUid);
  });

  it('returns null for unknown model', () => {
    assert.equal(getModelInfo('fake-model'), null);
  });
});

describe('getModelKeysByEnum', () => {
  it('returns keys for known enum', () => {
    const info = getModelInfo('gpt-4o');
    if (info?.enumValue) {
      const keys = getModelKeysByEnum(info.enumValue);
      assert.ok(keys.includes('gpt-4o'));
    }
  });

  it('returns empty array for unknown enum', () => {
    assert.deepEqual(getModelKeysByEnum(999999), []);
  });
});

describe('MODEL_TIER_ACCESS', () => {
  it('pro tier includes all models', () => {
    assert.ok(MODEL_TIER_ACCESS.pro.length > 100);
  });

  it('free tier is a small subset', () => {
    assert.ok(MODEL_TIER_ACCESS.free.length <= 5);
    assert.ok(MODEL_TIER_ACCESS.free.includes('gemini-2.5-flash'));
  });

  it('expired tier is empty', () => {
    assert.deepEqual(MODEL_TIER_ACCESS.expired, []);
  });
});

describe('deprecated model markers', () => {
  // Models the Windsurf upstream removed from Cascade. Requests for them
  // come back as "neither PlanModel nor RequestedModel specified" — we
  // catch that in handlers/chat.js with a 410 model_deprecated response.
  // If any of these loses its deprecated flag without the actual upstream
  // coming back, users will get the cryptic 502 again and reopen #8.
  const KNOWN_DEPRECATED = [
    'gpt-4o-mini', 'gpt-4.1-mini', 'gpt-4.1-nano', 'gpt-5-mini',
    'deepseek-v3', 'deepseek-v3-2', 'deepseek-r1',
    'grok-3-mini', 'qwen-3',
    // v2.0.51 #109 — verified broken in production:
    //   3.5/3.7 Claude legacy: "neither PlanModel nor RequestedModel specified"
    //   adaptive/arena-*: "unknown model UID ... model not found"
    'claude-3.5-sonnet', 'claude-3.7-sonnet', 'claude-3.7-sonnet-thinking',
    'adaptive', 'arena-fast', 'arena-smart',
  ];

  for (const key of KNOWN_DEPRECATED) {
    it(`${key} is flagged deprecated`, () => {
      const info = getModelInfo(key);
      assert.ok(info, `${key} missing from MODELS`);
      assert.equal(info.deprecated, true, `${key} lost its deprecated flag`);
    });
  }
});