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`);
});
}
});
|