import { describe, it, expect } from 'bun:test' import { normalizeModelId, lookupCatalog, resolveModelLimits, FALLBACK_CONTEXT, FALLBACK_OUTPUT, } from './modelCatalog.ts' import { MODEL_CATALOG } from './modelCatalog.generated.ts' describe('modelCatalog', () => { describe('normalizeModelId', () => { it('normalizes provider prefixed IDs', () => { expect(normalizeModelId('cc/claude-sonnet-4.5')).toBe('claude-sonnet-4-5') expect(normalizeModelId('anthropic/claude-opus-4-7')).toBe('claude-opus-4-7') }) it('normalizes bare IDs', () => { expect(normalizeModelId('claude-sonnet-4.5')).toBe('claude-sonnet-4-5') }) it('normalizes multi-tier IDs', () => { expect(normalizeModelId('openrouter/anthropic/claude-sonnet-4.5')).toBe('anthropic/claude-sonnet-4-5') }) it('handles uppercase characters', () => { expect(normalizeModelId('CC/Claude-Sonnet-4.5')).toBe('claude-sonnet-4-5') }) }) describe('lookupCatalog - verified IDs', () => { const verifiedMatches: [string, string][] = [ ['cc/claude-sonnet-4.5', 'anthropic/claude-sonnet-4-5'], ['kr/claude-sonnet-4.5', 'anthropic/claude-sonnet-4-5'], ['cc/claude-opus-4-5-20251101', 'anthropic/claude-opus-4-5-20251101'], ['cx/gpt-5.6-sol', 'openai/gpt-5.6-sol'], ['cx/gpt-5.3-codex', 'openai/gpt-5.3-codex'], ['ag/gemini-3.1-pro-preview', 'google/gemini-3.1-pro-preview'], ['vertex/gemini-3-flash-preview', 'google/gemini-3-flash-preview'], ['openai/gpt-4o', 'openai/gpt-4o'], ['openai/o3-mini', 'openai/o3-mini'], ['deepseek/deepseek-r1', 'deepseek/deepseek-r1'], ['cc/claude-opus-4-7', 'anthropic/claude-opus-4-7'], ] for (const [queryId, expectedCatalogKey] of verifiedMatches) { it(`resolves ${queryId} -> ${expectedCatalogKey}`, () => { const hit = lookupCatalog(queryId) expect(hit).not.toBeNull() expect(hit!.key).toBe(expectedCatalogKey) }) } it('returns null on unknown models', () => { expect(lookupCatalog('foo/definitely-not-real')).toBeNull() }) it('matches date suffix aliases (tier 4)', () => { // cc/claude-sonnet-4-6-20260101 does not exist in catalog, but anthropic/claude-sonnet-4-6 does const hit = lookupCatalog('cc/claude-sonnet-4-6-20260101') expect(hit).not.toBeNull() expect(hit!.key).toBe('anthropic/claude-sonnet-4-6') }) }) describe('catalog integrity & fuzz invariant', () => { const catalogKeys = Object.keys(MODEL_CATALOG) it('contains usable catalog entries', () => { expect(catalogKeys.length).toBeGreaterThanOrEqual(300) }) it('validates every entry in MODEL_CATALOG adheres to invariants', () => { for (const key of catalogKeys) { const entry = MODEL_CATALOG[key] expect(entry.c).toBeGreaterThan(0) if (entry.o !== undefined) { expect(entry.o).toBeGreaterThan(0) } if (entry.e !== undefined) { expect(entry.e.length).toBeGreaterThan(0) } const limits = resolveModelLimits({ id: key }) expect(limits.maxInputTokens).toBeGreaterThanOrEqual(1) expect(limits.maxOutputTokens).toBeGreaterThanOrEqual(1) expect(limits.maxInputTokens + limits.maxOutputTokens).toBeLessThanOrEqual(limits.contextWindow) expect(limits.source).toBe('models.dev') } }) }) describe('resolveModelLimits specifics', () => { it('preserves non-degenerate limits where output > context / 2 (zhipuai/glm-4.7)', () => { const hit = lookupCatalog('zhipuai/glm-4.7') if (hit) { const limits = resolveModelLimits({ id: 'zhipuai/glm-4.7' }) expect(limits.maxOutputTokens).toBe(131072) expect(limits.maxInputTokens).toBe(73728) expect(limits.maxInputTokens + limits.maxOutputTokens).toBe(204800) } }) it('preserves non-degenerate limits (openai/gpt-5-pro)', () => { const hit = lookupCatalog('openai/gpt-5-pro') if (hit) { const limits = resolveModelLimits({ id: 'openai/gpt-5-pro' }) expect(limits.maxOutputTokens).toBe(272000) expect(limits.maxInputTokens).toBe(128000) expect(limits.maxInputTokens + limits.maxOutputTokens).toBe(400000) } }) it('synthesizes degenerate output limit when declared output >= context (xai/grok-4.6)', () => { const hit = lookupCatalog('xai/grok-4.6') if (hit && hit.entry.o === hit.entry.c) { const limits = resolveModelLimits({ id: 'xai/grok-4.6' }) expect(limits.maxOutputTokens).toBe(125000) expect(limits.maxInputTokens).toBe(375000) expect(limits.maxInputTokens + limits.maxOutputTokens).toBe(500000) } }) it('follows precedence: models.dev wins over gateway arguments', () => { const limits = resolveModelLimits({ id: 'cc/claude-sonnet-4.5', contextWindow: 999999, maxOutputTokens: 1111, }) expect(limits.source).toBe('models.dev') expect(limits.contextWindow).toBe(200000) expect(limits.maxOutputTokens).toBe(64000) expect(limits.maxInputTokens).toBe(136000) }) it('uses gateway limits when uncataloged', () => { const limits = resolveModelLimits({ id: 'private/custom-model', contextWindow: 64000, maxOutputTokens: 8192, }) expect(limits.source).toBe('gateway') expect(limits.contextWindow).toBe(64000) expect(limits.maxOutputTokens).toBe(8192) expect(limits.maxInputTokens).toBe(55808) }) it('uses defaults when uncataloged and no gateway limits provided', () => { const limits = resolveModelLimits({ id: 'private/custom-model', }) expect(limits.source).toBe('default') expect(limits.contextWindow).toBe(FALLBACK_CONTEXT) expect(limits.maxOutputTokens).toBe(FALLBACK_OUTPUT) expect(limits.maxInputTokens).toBe(FALLBACK_CONTEXT - FALLBACK_OUTPUT) }) }) })