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.
152 lines
5.8 KiB
TypeScript
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');
|
|
});
|
|
});
|
|
});
|