mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-02 10:19:37 +00:00
fix(usage): normalize provider aliases in aggregation
This commit is contained in:
@@ -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];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user