Files
gemini-nexus/src/tests/unit/model-mapping.test.ts
T
evandro 880b799535 refactor: reduce cyclomatic complexity of request mapping and rate-limit parsing
Pin the current behavior with characterization tests (claude-request-mapper,
model-mapping, rate-limit-reason; 91 cases, all green before the refactor),
then split buildContents, buildGenerationConfig, transformClaudeRequestIn,
resolveModelRoute, detectRateLimitReason and computeRetryAfterSeconds into
small helpers. Also declare the emitted toolConfig on GeminiRequest.
2026-10-10 04:20:22 -03:00

152 lines
5.8 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import {
getDynamicForwardingTarget,
resolveModelRoute,
updateDynamicForwardingRules,
} from '../../lib/geminiNexus/ModelMapping';
describe('resolveModelRoute', () => {
const NO_MAPPING: Record<string, string> = {};
function route(
model: string,
options: {
custom?: Record<string, string>;
openai?: Record<string, string>;
anthropic?: Record<string, string>;
} = {},
) {
return resolveModelRoute(
model,
options.custom ?? NO_MAPPING,
options.openai ?? NO_MAPPING,
options.anthropic ?? NO_MAPPING,
);
}
describe('dynamic forwarding', () => {
it('forwards a deprecated model to a supported replacement', () => {
updateDynamicForwardingRules('route-test-old-model', 'gemini-3-flash');
expect(getDynamicForwardingTarget('route-test-old-model')).toBe('gemini-3-flash');
expect(route('route-test-old-model', { custom: { 'route-test-old-model': 'custom' } })).toBe(
'gemini-3-flash',
);
});
it('ignores a forwarding rule whose target is not a supported model', () => {
updateDynamicForwardingRules('route-test-unsupported', 'not-a-supported-model');
expect(route('route-test-unsupported')).toBe('route-test-unsupported');
});
it('ignores empty or non-string rules', () => {
updateDynamicForwardingRules(' ', 'gemini-3-flash');
updateDynamicForwardingRules('route-test-empty-target', ' ');
expect(getDynamicForwardingTarget('route-test-empty-target')).toBeUndefined();
});
});
describe('custom exact mapping', () => {
it('wins over family mappings and the built-in table', () => {
expect(
route('gpt-4o', {
custom: { 'gpt-4o': 'my-model' },
openai: { 'gpt-4o-series': 'other' },
}),
).toBe('my-model');
});
it('is case sensitive and does not apply to other names', () => {
expect(route('GPT-4O', { custom: { 'gpt-4o': 'my-model' } })).toBe('GPT-4O');
});
});
describe('OpenAI families', () => {
it('maps the GPT-4 series (classic, o1-, o3-) when configured', () => {
const openai = { 'gpt-4-series': 'gpt4-target' };
expect(route('gpt-4', { openai })).toBe('gpt4-target');
expect(route('gpt-4-0613', { openai })).toBe('gpt4-target');
expect(route('o1-preview', { openai })).toBe('gpt4-target');
expect(route('O3-mini', { openai })).toBe('gpt4-target');
});
it('does not treat gpt-4o, mini or turbo variants as the GPT-4 series', () => {
const openai = { 'gpt-4-series': 'gpt4-target' };
expect(route('gpt-4o', { openai })).toBe('gemini-3-flash');
expect(route('gpt-4-turbo', { openai })).toBe('gemini-3-flash');
});
it('maps the GPT-4o / 3.5 / mini / turbo series when configured', () => {
const openai = { 'gpt-4o-series': 'mini-target' };
expect(route('gpt-4o', { openai })).toBe('mini-target');
expect(route('gpt-3.5-turbo', { openai })).toBe('mini-target');
expect(route('gpt-4-turbo', { openai })).toBe('mini-target');
expect(route('gpt-4o-mini', { openai })).toBe('mini-target');
expect(route('some-mini-model', { openai })).toBe('mini-target');
});
it('does not treat gemini names containing "mini" as the GPT-4o series', () => {
const openai = { 'gpt-4o-series': 'mini-target' };
expect(route('gemini-3-flash', { openai })).toBe('gemini-3-flash');
});
it('maps GPT-5 to gpt-5-series, then falls back to gpt-4-series, then to the default', () => {
expect(route('gpt-5.2', { openai: { 'gpt-5-series': 'g5', 'gpt-4-series': 'g4' } })).toBe(
'g5',
);
expect(route('gpt-5', { openai: { 'gpt-4-series': 'g4' } })).toBe('g4');
expect(route('gpt-5', { openai: { 'gpt-4o-series': 'mini' } })).toBe('gpt-5');
});
it('falls through to the built-in table when the family key is not configured', () => {
expect(route('gpt-4')).toBe('gemini-3-flash');
expect(route('gpt-4', { openai: { 'gpt-4o-series': 'mini' } })).toBe('gemini-3-flash');
});
});
describe('Anthropic families', () => {
it('picks the 4.5, 3.5 or default family key from the model name', () => {
const anthropic = {
'claude-4.5-series': 'family-45',
'claude-3.5-series': 'family-35',
'claude-default': 'family-default',
};
expect(route('claude-sonnet-4-5-20250929', { anthropic })).toBe('family-45');
expect(route('claude-sonnet-4.5', { anthropic })).toBe('family-45');
expect(route('claude-3-5-sonnet-20241022', { anthropic })).toBe('family-35');
expect(route('claude-3.5-haiku', { anthropic })).toBe('family-35');
expect(route('claude-opus-4-6', { anthropic })).toBe('family-default');
});
it('falls back to a legacy exact mapping when the family key is missing', () => {
expect(route('claude-opus-4-6', { anthropic: { 'claude-opus-4-6': 'legacy-target' } })).toBe(
'legacy-target',
);
});
it('prefers the family key over the legacy exact mapping', () => {
expect(
route('claude-opus-4-6', {
anthropic: { 'claude-default': 'family-default', 'claude-opus-4-6': 'legacy-target' },
}),
).toBe('family-default');
});
it('is not applied to names that do not start with claude-', () => {
expect(route('my-claude-model', { anthropic: { 'claude-default': 'family-default' } })).toBe(
'my-claude-model',
);
});
it('falls through to the built-in table when nothing is configured', () => {
expect(route('claude-sonnet-4-5')).toBe('claude-sonnet-4-6-thinking');
});
});
describe('system default mapping', () => {
it('maps known aliases and passes unknown names through', () => {
expect(route('gemini-2.5-pro')).toBe('gemini-3.1-pro-high');
expect(route('totally-unknown-model')).toBe('totally-unknown-model');
});
});
});