Skip to content

Commit 0c8a327

Browse files
Nicolas-FormentonNicolas
andauthored
fix: honor specific-tier fallbacks in proxy path (#1587)
Co-authored-by: Nicolas <nicolas@polymarket-bot.local>
1 parent 313a332 commit 0c8a327

File tree

6 files changed

+53
-3
lines changed

6 files changed

+53
-3
lines changed

packages/backend/src/routing/dto/resolve-response.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,5 @@ export interface ResolveResponse {
1212
reason: ScoringReason;
1313
auth_type?: AuthType;
1414
specificity_category?: SpecificityCategory;
15+
fallback_models?: string[] | null;
1516
}

packages/backend/src/routing/proxy/__tests__/proxy.service.spec.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1761,6 +1761,52 @@ describe('ProxyService', () => {
17611761
expect(result.meta.fallbackIndex).toBeUndefined();
17621762
});
17631763

1764+
it('tries specificity fallback models before tier fallbacks when a specific-tier route fails', async () => {
1765+
resolveService.resolve.mockResolvedValue({
1766+
tier: 'standard',
1767+
model: 'gpt-5.4',
1768+
provider: 'OpenAI',
1769+
confidence: 0.95,
1770+
score: 0,
1771+
reason: 'specificity',
1772+
specificity_category: 'coding',
1773+
fallback_models: ['claude-sonnet-4'],
1774+
});
1775+
providerKeyService.getProviderApiKey
1776+
.mockResolvedValueOnce('sk-openai')
1777+
.mockResolvedValueOnce('sk-ant');
1778+
providerClient.forward
1779+
.mockResolvedValueOnce({
1780+
response: new Response('error', { status: 429 }),
1781+
isGoogle: false,
1782+
isAnthropic: false,
1783+
isChatGpt: false,
1784+
})
1785+
.mockResolvedValueOnce({
1786+
response: new Response('{}', { status: 200 }),
1787+
isGoogle: false,
1788+
isAnthropic: true,
1789+
isChatGpt: false,
1790+
});
1791+
tierService.getTiers.mockResolvedValue([
1792+
{ tier: 'standard', fallback_models: null },
1793+
] as never);
1794+
pricingCache.getByModel.mockReturnValue({ provider: 'Anthropic' } as never);
1795+
1796+
const result = await service.proxyRequest({
1797+
agentId: 'agent-1',
1798+
userId: 'user-1',
1799+
body,
1800+
sessionKey: 'default',
1801+
});
1802+
1803+
expect(result.meta.fallbackFromModel).toBe('gpt-5.4');
1804+
expect(result.meta.fallbackIndex).toBe(0);
1805+
expect(result.meta.primaryErrorStatus).toBe(429);
1806+
expect(result.meta.model).toBe('claude-sonnet-4');
1807+
expect(result.meta.specificity_category).toBe('coding');
1808+
});
1809+
17641810
it('tries fallback model when primary returns 429', async () => {
17651811
resolveService.resolve.mockResolvedValue({
17661812
tier: 'standard',

packages/backend/src/routing/proxy/proxy.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ export class ProxyService {
177177
if (!forward.response.ok && shouldTriggerFallback(forward.response.status)) {
178178
const tiers = await this.tierService.getTiers(agentId);
179179
const assignment = tiers.find((t) => t.tier === resolved.tier);
180-
const fallbackModels = assignment?.fallback_models;
180+
const fallbackModels = resolved.fallback_models ?? assignment?.fallback_models;
181181

182182
if (fallbackModels && fallbackModels.length > 0) {
183183
const primaryStatus = forward.response.status;

packages/backend/src/routing/resolve.service.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,7 @@ describe('ResolveService', () => {
421421
override_provider: 'anthropic',
422422
override_auth_type: null,
423423
auto_assigned_model: null,
424+
fallback_models: ['gpt-4o', 'deepseek-chat'],
424425
},
425426
]);
426427
mockProviderKeyService.hasActiveProvider.mockResolvedValue(true);
@@ -434,6 +435,7 @@ describe('ResolveService', () => {
434435
expect(result.provider).toBe('anthropic');
435436
expect(result.reason).toBe('specificity');
436437
expect(result.specificity_category).toBe('coding');
438+
expect(result.fallback_models).toEqual(['gpt-4o', 'deepseek-chat']);
437439
});
438440

439441
it('should return null when no active assignments exist', async () => {

packages/backend/src/routing/resolve/resolve.service.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ export class ResolveService {
162162
reason: 'specificity',
163163
auth_type: authType,
164164
specificity_category: detected.category,
165+
fallback_models: assignment.fallback_models ?? null,
165166
};
166167
}
167168

packages/backend/src/routing/routing-core/tier.service.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { RoutingCacheService } from './routing-cache.service';
88
import { ProviderService } from './provider.service';
99
import { ModelDiscoveryService } from '../../model-discovery/model-discovery.service';
1010
import { randomUUID } from 'crypto';
11-
import { TIERS } from '../../scoring/types';
11+
import { TIERS, Tier } from '../../scoring/types';
1212
import { isManifestUsableProvider } from '../../common/utils/subscription-support';
1313

1414
@Injectable()
@@ -39,7 +39,7 @@ export class TierService {
3939

4040
if (rows.length === 0) {
4141
// Batch tier inserts — create all 4 tier rows in one query
42-
const created: TierAssignment[] = TIERS.map((tier) =>
42+
const created: TierAssignment[] = TIERS.map((tier: Tier) =>
4343
Object.assign(new TierAssignment(), {
4444
id: randomUUID(),
4545
user_id: userId ?? '',

0 commit comments

Comments
 (0)