Skip to content

Commit d673edc

Browse files
authored
Merge pull request #1279 from mnfst/chore/model-pricing-cache-cleanup
chore: clean up model pricing cache
2 parents c205266 + 66412b2 commit d673edc

13 files changed

+40
-158
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"manifest": patch
3+
---
4+
5+
Clean up model pricing cache: return ReadonlyMap from PricingSyncService, remove dead code, derive provider prefixes from registry, fix naming inconsistencies

packages/backend/src/database/pricing-sync.service.spec.ts

Lines changed: 0 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -414,95 +414,6 @@ describe('PricingSyncService', () => {
414414
});
415415
});
416416

417-
describe('deriveNames', () => {
418-
it('maps known providers correctly', () => {
419-
expect(service.deriveNames('anthropic/claude-opus-4')).toEqual({
420-
canonical: 'claude-opus-4',
421-
provider: 'Anthropic',
422-
});
423-
expect(service.deriveNames('openai/gpt-4o')).toEqual({
424-
canonical: 'gpt-4o',
425-
provider: 'OpenAI',
426-
});
427-
expect(service.deriveNames('google/gemini-2.5-pro')).toEqual({
428-
canonical: 'gemini-2.5-pro',
429-
provider: 'Google',
430-
});
431-
expect(service.deriveNames('deepseek/deepseek-r1')).toEqual({
432-
canonical: 'deepseek-r1',
433-
provider: 'DeepSeek',
434-
});
435-
expect(service.deriveNames('mistralai/mistral-large')).toEqual({
436-
canonical: 'mistral-large',
437-
provider: 'Mistral',
438-
});
439-
// meta-llama is not a supported routing provider — title-cased
440-
expect(service.deriveNames('meta-llama/llama-3')).toEqual({
441-
canonical: 'llama-3',
442-
provider: 'Meta-llama',
443-
});
444-
expect(service.deriveNames('cohere/command-r')).toEqual({
445-
canonical: 'command-r',
446-
provider: 'Cohere',
447-
});
448-
expect(service.deriveNames('xai/grok-2')).toEqual({
449-
canonical: 'grok-2',
450-
provider: 'xAI',
451-
});
452-
expect(service.deriveNames('moonshotai/moonshot-v1')).toEqual({
453-
canonical: 'moonshot-v1',
454-
provider: 'Moonshot',
455-
});
456-
expect(service.deriveNames('qwen/qwen3-235b-a22b')).toEqual({
457-
canonical: 'qwen3-235b-a22b',
458-
provider: 'Alibaba',
459-
});
460-
expect(service.deriveNames('zhipuai/glm-4-plus')).toEqual({
461-
canonical: 'glm-4-plus',
462-
provider: 'Z.ai',
463-
});
464-
expect(service.deriveNames('amazon/nova-pro')).toEqual({
465-
canonical: 'nova-pro',
466-
provider: 'Amazon',
467-
});
468-
});
469-
470-
it('preserves full ID for openrouter/ models', () => {
471-
expect(service.deriveNames('openrouter/auto')).toEqual({
472-
canonical: 'openrouter/auto',
473-
provider: 'OpenRouter',
474-
});
475-
});
476-
477-
it('maps MiniMax provider correctly', () => {
478-
expect(service.deriveNames('minimax/minimax-m2.5')).toEqual({
479-
canonical: 'minimax-m2.5',
480-
provider: 'MiniMax',
481-
});
482-
});
483-
484-
it('maps Z.ai provider correctly', () => {
485-
expect(service.deriveNames('z-ai/glm-5')).toEqual({
486-
canonical: 'glm-5',
487-
provider: 'Z.ai',
488-
});
489-
});
490-
491-
it('title-cases unknown providers', () => {
492-
expect(service.deriveNames('newvendor/some-model')).toEqual({
493-
canonical: 'some-model',
494-
provider: 'Newvendor',
495-
});
496-
});
497-
498-
it('handles model IDs without slash', () => {
499-
expect(service.deriveNames('bare-model')).toEqual({
500-
canonical: 'bare-model',
501-
provider: 'Unknown',
502-
});
503-
});
504-
});
505-
506417
describe('extractDisplayName', () => {
507418
it('strips vendor prefix from OpenRouter names', () => {
508419
expect(

packages/backend/src/database/pricing-sync.service.ts

Lines changed: 1 addition & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
22
import { Cron, CronExpression } from '@nestjs/schedule';
3-
import { OPENROUTER_PREFIX_TO_PROVIDER } from '../common/constants/providers';
43

54
interface OpenRouterModel {
65
id: string;
@@ -87,7 +86,7 @@ export class PricingSyncService implements OnModuleInit {
8786
return this.cache.get(modelId) ?? null;
8887
}
8988

90-
getAll(): Map<string, OpenRouterPricingEntry> {
89+
getAll(): ReadonlyMap<string, OpenRouterPricingEntry> {
9190
return this.cache;
9291
}
9392

@@ -117,30 +116,6 @@ export class PricingSyncService implements OnModuleInit {
117116
return model.name;
118117
}
119118

120-
deriveNames(openRouterId: string): {
121-
canonical: string;
122-
provider: string;
123-
} {
124-
if (openRouterId.startsWith('openrouter/')) {
125-
return { canonical: openRouterId, provider: 'OpenRouter' };
126-
}
127-
128-
const slashIndex = openRouterId.indexOf('/');
129-
if (slashIndex === -1) {
130-
return { canonical: openRouterId, provider: 'Unknown' };
131-
}
132-
133-
const prefix = openRouterId.substring(0, slashIndex);
134-
const canonical = openRouterId.substring(slashIndex + 1);
135-
const provider = OPENROUTER_PREFIX_TO_PROVIDER.get(prefix) ?? this.titleCase(prefix);
136-
137-
return { canonical, provider };
138-
}
139-
140-
private titleCase(str: string): string {
141-
return str.charAt(0).toUpperCase() + str.slice(1);
142-
}
143-
144119
isChatCompatible(model: OpenRouterModel): boolean {
145120
const inputModalities = model.architecture?.input_modalities?.map((m) => m.toLowerCase());
146121
if (inputModalities && inputModalities.length > 0 && !inputModalities.includes('text')) {

packages/backend/src/model-prices/model-name-normalizer.ts

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { buildAnthropicShortModelIdVariants } from '../common/utils/anthropic-model-id';
2+
import { OPENROUTER_PREFIX_TO_PROVIDER } from '../common/constants/providers';
23

34
/**
45
* Resolves variant model names (from telemetry) to canonical pricing names.
@@ -39,24 +40,21 @@ const KNOWN_ALIASES: ReadonlyArray<readonly [string, string]> = [
3940
['codestral', 'codestral-latest'],
4041
];
4142

42-
const PROVIDER_PREFIXES = [
43-
'anthropic/',
44-
'openai/',
45-
'google/',
46-
'deepseek/',
47-
'mistralai/',
48-
'moonshotai/',
49-
'qwen/',
50-
'zhipuai/',
51-
'amazon/',
52-
'xai/',
53-
'minimax/',
54-
'z-ai/',
43+
// Extra prefixes from non-routing providers that telemetry may send.
44+
// Multi-segment prefixes must come first so they match before shorter ones.
45+
const EXTRA_TELEMETRY_PREFIXES = [
5546
'accounts/fireworks/models/',
47+
'amazon/',
5648
'fireworks/',
5749
'together/',
5850
];
5951

52+
/** Derived from the provider registry + extra telemetry prefixes. */
53+
const PROVIDER_PREFIXES: readonly string[] = [
54+
...EXTRA_TELEMETRY_PREFIXES,
55+
...[...OPENROUTER_PREFIX_TO_PROVIDER.keys()].map((p) => `${p}/`),
56+
];
57+
6058
const DATE_SUFFIX_RE = /-\d{4}-?\d{2}-?\d{2}$/;
6159

6260
export function stripProviderPrefix(name: string): string {

packages/backend/src/model-prices/model-prices.controller.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ describe('ModelPricesController', () => {
1414

1515
it('delegates to service.getAll()', async () => {
1616
const expected = { models: [], lastSyncedAt: null };
17-
mockService.getAll.mockResolvedValue(expected);
17+
mockService.getAll.mockReturnValue(expected);
1818

1919
const result = await controller.getModelPrices();
2020

packages/backend/src/model-prices/model-prices.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export class ModelPricesService {
99
private readonly pricingSync: PricingSyncService,
1010
) {}
1111

12-
async getAll() {
12+
getAll() {
1313
const entries = this.pricingCache.getAll();
1414
const lastSyncedAt = this.pricingSync.getLastFetchedAt()?.toISOString() ?? null;
1515

packages/backend/src/model-prices/model-pricing-cache.service.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ describe('ModelPricingCacheService', () => {
2929
);
3030
});
3131

32-
describe('onModuleInit', () => {
32+
describe('onApplicationBootstrap', () => {
3333
it('should call reload()', async () => {
3434
const spy = jest.spyOn(service, 'reload').mockResolvedValue();
3535
await service.onApplicationBootstrap();
@@ -196,7 +196,7 @@ describe('ModelPricingCacheService', () => {
196196
await service.reload();
197197

198198
const result = service.getAll();
199-
expect(result.length).toBeGreaterThanOrEqual(2);
199+
expect(result.length).toBe(2);
200200
const names = result.map((e) => e.model_name);
201201
expect(names).toContain('openai/gpt-4o');
202202
expect(names).toContain('anthropic/claude-opus-4-6');

packages/backend/src/model-prices/model-pricing-cache.service.ts

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -45,30 +45,22 @@ export class ModelPricingCacheService implements OnApplicationBootstrap {
4545
for (const [fullId, entry] of orCache) {
4646
const { provider, canonical, providerId } = this.resolveProviderAndName(fullId);
4747

48-
const displayName = entry.displayName ?? null;
49-
const validated = this.resolveValidated(providerId, canonical);
50-
51-
// Store under full OpenRouter ID (e.g. "anthropic/claude-opus-4-6")
52-
this.cache.set(fullId, {
48+
const pricingEntry: PricingEntry = {
5349
model_name: fullId,
5450
provider,
5551
input_price_per_token: entry.input,
5652
output_price_per_token: entry.output,
57-
display_name: displayName,
58-
validated,
59-
});
53+
display_name: entry.displayName ?? null,
54+
validated: this.resolveValidated(providerId, canonical),
55+
};
56+
57+
// Store under full OpenRouter ID (e.g. "anthropic/claude-opus-4-6")
58+
this.cache.set(fullId, pricingEntry);
6059

6160
// For supported providers, also store under canonical name (e.g. "claude-opus-4-6")
6261
// so cost lookups work when telemetry sends bare model names
6362
if (canonical !== fullId && !this.cache.has(canonical)) {
64-
this.cache.set(canonical, {
65-
model_name: fullId,
66-
provider,
67-
input_price_per_token: entry.input,
68-
output_price_per_token: entry.output,
69-
display_name: displayName,
70-
validated,
71-
});
63+
this.cache.set(canonical, pricingEntry);
7264
}
7365
}
7466

@@ -121,10 +113,10 @@ export class ModelPricingCacheService implements OnApplicationBootstrap {
121113
}
122114

123115
const prefix = openRouterId.substring(0, slashIdx);
124-
const displayName = OPENROUTER_PREFIX_TO_PROVIDER.get(prefix);
125-
if (displayName) {
116+
const providerDisplayName = OPENROUTER_PREFIX_TO_PROVIDER.get(prefix);
117+
if (providerDisplayName) {
126118
return {
127-
provider: displayName,
119+
provider: providerDisplayName,
128120
canonical: openRouterId.substring(slashIdx + 1),
129121
providerId: prefix,
130122
};

packages/backend/src/routing/model-discovery/model-fallback.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ interface PricingLookup {
1616
contextWindow?: number;
1717
displayName?: string;
1818
} | null;
19-
getAll(): Map<
19+
getAll(): ReadonlyMap<
2020
string,
2121
{ input: number; output: number; contextWindow?: number; displayName?: string }
2222
>;

packages/backend/test/costs.e2e-spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ beforeAll(async () => {
1919

2020
// Populate PricingSyncService cache with gpt-4o pricing
2121
const pricingSync = app.get(PricingSyncService);
22-
pricingSync.getAll().set('openai/gpt-4o', {
22+
(pricingSync.getAll() as Map<string, { input: number; output: number; contextWindow?: number }>).set('openai/gpt-4o', {
2323
input: 0.0000025,
2424
output: 0.00001,
2525
contextWindow: 128000,

0 commit comments

Comments
 (0)