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.
This commit is contained in:
1 parent
cf5b669133
commit
880b799535
7 files changed
+1630
-449
No files matched your search
@@ -0,0 +1,151 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user