fix(usage): normalize provider aliases in aggregation

This commit is contained in:
Tam Nhu Tran
2026-04-28 15:43:13 -04:00
parent a538627933
commit fc90b6f473
5 changed files with 65 additions and 11 deletions
@@ -36,7 +36,9 @@ function normalizeId(value: string): string {
return value.trim().toLowerCase(); return value.trim().toLowerCase();
} }
function normalizeProvider(provider: string | undefined): string | undefined { export function normalizeModelsDevProviderId(
provider: string | null | undefined
): string | undefined {
if (!provider) return undefined; if (!provider) return undefined;
const normalized = normalizeId(provider); const normalized = normalizeId(provider);
return PROVIDER_ALIASES[normalized] ?? normalized; return PROVIDER_ALIASES[normalized] ?? normalized;
@@ -78,7 +80,7 @@ function findProvider(
registry: ModelsDevRegistry, registry: ModelsDevRegistry,
provider: string | undefined provider: string | undefined
): ModelsDevProvider | undefined { ): ModelsDevProvider | undefined {
const normalizedProvider = normalizeProvider(provider); const normalizedProvider = normalizeModelsDevProviderId(provider);
if (!normalizedProvider) return undefined; if (!normalizedProvider) return undefined;
return registry[normalizedProvider]; return registry[normalizedProvider];
} }
+2 -2
View File
@@ -14,7 +14,7 @@ import {
type MonthlyUsage, type MonthlyUsage,
type SessionUsage, type SessionUsage,
} from './types'; } from './types';
import { getModelsUsed } from './model-identity'; import { getModelsUsed, normalizeUsageProvider } from './model-identity';
// ============================================================================ // ============================================================================
// HELPER FUNCTIONS // HELPER FUNCTIONS
@@ -74,7 +74,7 @@ interface ModelAccumulator {
} }
function getEntryProvider(entry: RawUsageEntry): string | undefined { function getEntryProvider(entry: RawUsageEntry): string | undefined {
return entry.target?.trim().toLowerCase() || undefined; return normalizeUsageProvider(entry.target);
} }
function getEntryModelKey(entry: RawUsageEntry): string { function getEntryModelKey(entry: RawUsageEntry): string {
+7 -1
View File
@@ -1,10 +1,16 @@
import { normalizeModelsDevProviderId } from '../models-dev/pricing-resolver';
export interface ProviderModelIdentity { export interface ProviderModelIdentity {
modelName: string; modelName: string;
provider?: string; provider?: string;
} }
export function normalizeUsageProvider(provider: string | undefined): string | undefined {
return normalizeModelsDevProviderId(provider);
}
function getProviderKey(provider: string | undefined): string { function getProviderKey(provider: string | undefined): string {
return provider?.trim().toLowerCase() ?? ''; return normalizeUsageProvider(provider) ?? '';
} }
function getModelUsageLabel(item: ProviderModelIdentity, ambiguousModelNames: Set<string>): string { function getModelUsageLabel(item: ProviderModelIdentity, ambiguousModelNames: Set<string>): string {
@@ -12,6 +12,8 @@ import {
import { resolveOpenAICompatProxyPreferredPort } from '../../../src/proxy/proxy-port-resolver'; import { resolveOpenAICompatProxyPreferredPort } from '../../../src/proxy/proxy-port-resolver';
import { resolveOpenAICompatProfileConfig } from '../../../src/proxy/profile-router'; import { resolveOpenAICompatProfileConfig } from '../../../src/proxy/profile-router';
import { import {
OPENAI_COMPAT_PROXY_ADAPTIVE_PORT_END,
OPENAI_COMPAT_PROXY_ADAPTIVE_PORT_START,
getLegacyOpenAICompatProxyPidPath, getLegacyOpenAICompatProxyPidPath,
getLegacyOpenAICompatProxySessionPath, getLegacyOpenAICompatProxySessionPath,
getOpenAICompatProxyPidPath, getOpenAICompatProxyPidPath,
@@ -38,6 +40,37 @@ afterEach(async () => {
fs.rmSync(tempDir, { recursive: true, force: true }); fs.rmSync(tempDir, { recursive: true, force: true });
}); });
async function findProfileNameWithFreeAdaptivePort(prefix: string): Promise<string> {
for (let index = 0; index < 200; index += 1) {
const profileName = `${prefix}-${index}`;
const preferredPort = resolveOpenAICompatProxyPreferredPort(profileName);
const availablePort = await getPort({ port: preferredPort, host: '127.0.0.1' });
if (availablePort === preferredPort) {
return profileName;
}
}
throw new Error(`No free adaptive proxy port found for ${prefix}`);
}
async function getPortOutsideOpenAICompatAdaptiveRange(): Promise<number> {
for (let attempt = 0; attempt < 10; attempt += 1) {
const rangeStart = 45_000 + attempt * 101;
const port = await getPort({
port: getPort.makeRange(rangeStart, rangeStart + 100),
host: '127.0.0.1',
});
if (
port < OPENAI_COMPAT_PROXY_ADAPTIVE_PORT_START ||
port > OPENAI_COMPAT_PROXY_ADAPTIVE_PORT_END
) {
return port;
}
}
throw new Error('No stale proxy fixture port found outside the adaptive range');
}
describe('openai proxy daemon lifecycle', () => { describe('openai proxy daemon lifecycle', () => {
it('starts, reports status, serves health/models, and stops', async () => { it('starts, reports status, serves health/models, and stops', async () => {
const port = await getPort(); const port = await getPort();
@@ -444,8 +477,9 @@ describe('openai proxy daemon lifecycle', () => {
}); });
it('returns to the adaptive canonical port after a stale fallback session', async () => { it('returns to the adaptive canonical port after a stale fallback session', async () => {
const stalePort = await getPort(); const profileName = await findProfileNameWithFreeAdaptivePort('outside-range');
const settingsPath = path.join(tempDir, 'outside-range.settings.json'); const stalePort = await getPortOutsideOpenAICompatAdaptiveRange();
const settingsPath = path.join(tempDir, `${profileName}.settings.json`);
fs.writeFileSync( fs.writeFileSync(
settingsPath, settingsPath,
JSON.stringify({ JSON.stringify({
@@ -459,7 +493,7 @@ describe('openai proxy daemon lifecycle', () => {
'utf8' 'utf8'
); );
const profile = resolveOpenAICompatProfileConfig('outside-range', settingsPath, { const profile = resolveOpenAICompatProfileConfig(profileName, settingsPath, {
ANTHROPIC_BASE_URL: 'http://127.0.0.1:11434', ANTHROPIC_BASE_URL: 'http://127.0.0.1:11434',
ANTHROPIC_AUTH_TOKEN: 'ollama-outside-range', ANTHROPIC_AUTH_TOKEN: 'ollama-outside-range',
ANTHROPIC_MODEL: 'qwen3-coder', ANTHROPIC_MODEL: 'qwen3-coder',
@@ -469,9 +503,9 @@ describe('openai proxy daemon lifecycle', () => {
throw new Error('Expected an outside-range OpenAI-compatible profile'); throw new Error('Expected an outside-range OpenAI-compatible profile');
} }
fs.mkdirSync(path.dirname(getOpenAICompatProxySessionPath('outside-range')), { recursive: true }); fs.mkdirSync(path.dirname(getOpenAICompatProxySessionPath(profileName)), { recursive: true });
fs.writeFileSync( fs.writeFileSync(
getOpenAICompatProxySessionPath('outside-range'), getOpenAICompatProxySessionPath(profileName),
JSON.stringify( JSON.stringify(
{ {
profileName: profile.profileName, profileName: profile.profileName,
@@ -490,7 +524,7 @@ describe('openai proxy daemon lifecycle', () => {
const started = await startOpenAICompatProxy(profile); const started = await startOpenAICompatProxy(profile);
expect(started.success).toBe(true); expect(started.success).toBe(true);
expect(started.port).toBe(resolveOpenAICompatProxyPreferredPort('outside-range')); expect(started.port).toBe(resolveOpenAICompatProxyPreferredPort(profileName));
expect(started.port).not.toBe(stalePort); expect(started.port).not.toBe(stalePort);
}); });
+12
View File
@@ -138,6 +138,18 @@ describe('aggregateDailyUsage', () => {
expect(result[0].modelsUsed).toEqual(['gpt-5.5']); expect(result[0].modelsUsed).toEqual(['gpt-5.5']);
}); });
test('canonicalizes provider aliases before grouping usage', () => {
const result = aggregateDailyUsage([
createEntry({ model: 'gpt-5.5', target: 'ghcp', inputTokens: 1000 }),
createEntry({ model: 'gpt-5.5', target: 'github-copilot', inputTokens: 2000 }),
]);
expect(result[0].modelsUsed).toEqual(['gpt-5.5']);
expect(result[0].modelBreakdowns).toHaveLength(1);
expect(result[0].modelBreakdowns[0].provider).toBe('github-copilot');
expect(result[0].modelBreakdowns[0].inputTokens).toBe(3000);
});
}); });
// ============================================================================ // ============================================================================