Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion src/features/monitoring/hooks/useMonitoringData.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { describe, expect, it } from 'vitest';
import { buildAccountRows, type MonitoringEventRow } from './useMonitoringData';
import {
buildAccountRows,
buildMonitoringAuthMetaMap,
type MonitoringEventRow,
} from './useMonitoringData';
import type { AuthFileItem } from '@/types';

const createMonitoringEventRow = (
overrides: Partial<MonitoringEventRow> = {}
Expand Down Expand Up @@ -55,3 +60,22 @@ describe('buildAccountRows', () => {
expect(rows[0].authIndices).toEqual(['auth-123456', 'auth-999999']);
});
});

describe('buildMonitoringAuthMetaMap', () => {
it('maps legacy auth indices to current auth metadata', () => {
const authFiles: AuthFileItem[] = [
{
name: 'alice.json',
provider: 'codex',
authIndex: 'current-auth-index',
path: '/tmp/auths/alice.json',
account: 'alice@example.com',
},
];

const map = buildMonitoringAuthMetaMap(authFiles);

expect(map.get('current-auth-index')?.account).toBe('alice@example.com');
expect(map.get('6bf749cb7db0e15c')?.account).toBe('alice@example.com');
});
});
71 changes: 52 additions & 19 deletions src/features/monitoring/hooks/useMonitoringData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { AuthFileItem } from '@/types/authFile';
import type { Config } from '@/types/config';
import type { CredentialInfo } from '@/types/sourceInfo';
import { buildSourceInfoMap, resolveSourceDisplay } from '@/utils/sourceResolver';
import { buildLegacyAuthIndexAliases } from '../legacyAuthIndexAliases';
import {
calculateCost,
collectUsageDetailsWithEndpoint,
Expand Down Expand Up @@ -547,6 +548,24 @@ const normalizeAuthMeta = (entry: AuthFileItem): MonitoringAuthMeta | null => {
};
};

export const buildMonitoringAuthMetaMap = (
authFiles: AuthFileItem[]
): Map<string, MonitoringAuthMeta> => {
const map = new Map<string, MonitoringAuthMeta>();
authFiles.forEach((entry) => {
const normalized = normalizeAuthMeta(entry);
if (!normalized) return;

map.set(normalized.authIndex, normalized);
buildLegacyAuthIndexAliases(entry).forEach((alias) => {
if (!map.has(alias)) {
map.set(alias, normalized);
}
});
});
return map;
};

const buildRangeFilteredRows = (
rows: MonitoringEventRow[],
timeRange: MonitoringTimeRange,
Expand Down Expand Up @@ -1353,12 +1372,26 @@ const buildEventRows = (
const authIndex = normalizeAuthIndex(detail.auth_index) ?? '-';
const authMeta = authMetaMap.get(authIndex);
const sourceMeta = resolveSourceDisplay(detail.source, detail.auth_index, sourceInfoMap, authFileMap);
const sourceLabel = authMeta?.label || sourceMeta.displayName || authIndex;
const snapshotAccount = readString(detail.account_snapshot ?? detail.accountSnapshot);
const snapshotLabel = readString(
detail.auth_label_snapshot ??
detail.authLabelSnapshot ??
detail.auth_file_snapshot ??
detail.authFileSnapshot
);
const snapshotProvider = readString(
detail.auth_provider_snapshot ?? detail.authProviderSnapshot
);
const snapshotDisplay = snapshotAccount || snapshotLabel;
const sourceLabel = authMeta?.label || snapshotDisplay || sourceMeta.displayName || authIndex;
const sourceMasked = maskEmailLike(sourceLabel);
const account = authMeta?.account || sourceLabel;
const account = authMeta?.account || snapshotAccount || sourceLabel;
const accountMasked = maskEmailLike(account);
const channelMeta = channelByAuthIndex.get(authIndex);
const channelLabel = channelMeta?.name || authMeta?.provider || sourceMeta.type || '-';
const channelMeta =
channelByAuthIndex.get(authIndex) ||
(authMeta?.authIndex ? channelByAuthIndex.get(authMeta.authIndex) : undefined);
const channelLabel =
channelMeta?.name || authMeta?.provider || snapshotProvider || sourceMeta.type || '-';
const endpoint = readString(detail.__endpoint) || '-';
const endpointMethod = readString(detail.__endpointMethod) || '-';
const endpointPath = readString(detail.__endpointPath) || endpoint;
Expand Down Expand Up @@ -1394,8 +1427,8 @@ const buildEventRows = (
accountMasked,
authIndex,
authIndexMasked: maskAuthIndex(authIndex),
authLabel: authMeta?.label || sourceMasked,
provider: authMeta?.provider || sourceMeta.type || '-',
authLabel: authMeta?.label || snapshotLabel || sourceMasked,
provider: authMeta?.provider || snapshotProvider || sourceMeta.type || '-',
planType: authMeta?.planType || '-',
channel: channelLabel,
channelHost: channelMeta?.host || '-',
Expand All @@ -1420,7 +1453,7 @@ const buildEventRows = (
channelMeta?.host,
endpointPath,
endpointMethod,
authMeta?.provider,
authMeta?.provider || snapshotProvider,
authMeta?.planType
),
} satisfies MonitoringEventRow;
Expand Down Expand Up @@ -1514,15 +1547,15 @@ export function useMonitoringData({
};
}, [config]);

const authMetaMap = useMemo(() => {
const authMetaMap = useMemo(() => buildMonitoringAuthMetaMap(authFiles), [authFiles]);

const uniqueAuthMeta = useMemo(() => {
const map = new Map<string, MonitoringAuthMeta>();
authFiles.forEach((entry) => {
const normalized = normalizeAuthMeta(entry);
if (!normalized) return;
map.set(normalized.authIndex, normalized);
authMetaMap.forEach((item) => {
map.set(item.authIndex, item);
});
return map;
}, [authFiles]);
return Array.from(map.values());
}, [authMetaMap]);

const authFileMap = useMemo(() => {
const map = new Map<string, CredentialInfo>();
Expand Down Expand Up @@ -1592,22 +1625,22 @@ export function useMonitoringData({

const metadata = useMemo<MonitoringMetadata>(() => {
const planTypes = Array.from(
new Set(Array.from(authMetaMap.values()).map((item) => item.planType).filter((item) => item && item !== '-'))
new Set(uniqueAuthMeta.map((item) => item.planType).filter((item) => item && item !== '-'))
).sort();

return {
totalAuthFiles: authFiles.length,
activeAuthFiles: Array.from(authMetaMap.values()).filter(
activeAuthFiles: uniqueAuthMeta.filter(
(item) => !item.disabled && !item.unavailable && item.status === 'active'
).length,
unavailableAuthFiles: Array.from(authMetaMap.values()).filter((item) => item.unavailable).length,
runtimeOnlyAuthFiles: Array.from(authMetaMap.values()).filter((item) => item.runtimeOnly).length,
unavailableAuthFiles: uniqueAuthMeta.filter((item) => item.unavailable).length,
runtimeOnlyAuthFiles: uniqueAuthMeta.filter((item) => item.runtimeOnly).length,
totalChannels: channels.length,
enabledChannels: channels.filter((item) => !item.disabled).length,
configuredModels: Array.from(new Set(channels.flatMap((item) => item.modelNames))).length,
planTypes,
};
}, [authFiles.length, authMetaMap, channels]);
}, [authFiles.length, channels, uniqueAuthMeta]);

const statusChips = useMemo(() => buildStatusChips(metadata), [metadata]);

Expand Down
21 changes: 21 additions & 0 deletions src/features/monitoring/legacyAuthIndexAliases.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { describe, expect, it } from 'vitest';
import { buildLegacyAuthIndexAliases, stableAuthIndexFromSeed } from './legacyAuthIndexAliases';

describe('legacy auth index aliases', () => {
it('matches CPA stable auth index hashing', () => {
expect(stableAuthIndexFromSeed('abc')).toBe('ba7816bf8f01cfea');
});

it('builds legacy file-based source aliases', () => {
const aliases = buildLegacyAuthIndexAliases({
name: 'alice.json',
provider: 'codex',
path: '/tmp/auths/alice.json',
authIndex: 'current-auth-index',
account: 'alice@example.com',
});

expect(aliases).toContain('6bf749cb7db0e15c');
expect(aliases).toContain('b2035f866a8fdbf7');
});
});
186 changes: 186 additions & 0 deletions src/features/monitoring/legacyAuthIndexAliases.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import type { AuthFileItem } from '@/types/authFile';

type RecordLike = Record<string, unknown>;

const SHA256_K = new Uint32Array([
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4,
0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe,
0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f,
0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc,
0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116,
0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7,
0xc67178f2,
]);

const readString = (value: unknown): string =>
value === null || value === undefined ? '' : String(value).trim();

const readAnyString = (entry: RecordLike, keys: string[]): string => {
for (const key of keys) {
const value = readString(entry[key]);
if (value) return value;
}
return '';
};

const rightRotate = (value: number, bits: number): number =>
(value >>> bits) | (value << (32 - bits));

export const stableAuthIndexFromSeed = (seed: string): string => {
const trimmed = seed.trim();
if (!trimmed) return '';

const bytes = new TextEncoder().encode(trimmed);
const bitLength = bytes.length * 8;
const paddedLength = Math.ceil((bytes.length + 9) / 64) * 64;
const data = new Uint8Array(paddedLength);
data.set(bytes);
data[bytes.length] = 0x80;

const view = new DataView(data.buffer);
view.setUint32(paddedLength - 8, Math.floor(bitLength / 0x100000000), false);
view.setUint32(paddedLength - 4, bitLength >>> 0, false);

const hash = new Uint32Array([
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab,
0x5be0cd19,
]);
const words = new Uint32Array(64);

for (let offset = 0; offset < paddedLength; offset += 64) {
for (let index = 0; index < 16; index += 1) {
words[index] = view.getUint32(offset + index * 4, false);
}
for (let index = 16; index < 64; index += 1) {
const s0 =
rightRotate(words[index - 15], 7) ^
rightRotate(words[index - 15], 18) ^
(words[index - 15] >>> 3);
const s1 =
rightRotate(words[index - 2], 17) ^
rightRotate(words[index - 2], 19) ^
(words[index - 2] >>> 10);
words[index] = (words[index - 16] + s0 + words[index - 7] + s1) >>> 0;
}

let a = hash[0];
let b = hash[1];
let c = hash[2];
let d = hash[3];
let e = hash[4];
let f = hash[5];
let g = hash[6];
let h = hash[7];

for (let index = 0; index < 64; index += 1) {
const s1 = rightRotate(e, 6) ^ rightRotate(e, 11) ^ rightRotate(e, 25);
const ch = (e & f) ^ (~e & g);
const temp1 = (h + s1 + ch + SHA256_K[index] + words[index]) >>> 0;
const s0 = rightRotate(a, 2) ^ rightRotate(a, 13) ^ rightRotate(a, 22);
const maj = (a & b) ^ (a & c) ^ (b & c);
const temp2 = (s0 + maj) >>> 0;

h = g;
g = f;
f = e;
e = (d + temp1) >>> 0;
d = c;
c = b;
b = a;
a = (temp1 + temp2) >>> 0;
}

hash[0] = (hash[0] + a) >>> 0;
hash[1] = (hash[1] + b) >>> 0;
hash[2] = (hash[2] + c) >>> 0;
hash[3] = (hash[3] + d) >>> 0;
hash[4] = (hash[4] + e) >>> 0;
hash[5] = (hash[5] + f) >>> 0;
hash[6] = (hash[6] + g) >>> 0;
hash[7] = (hash[7] + h) >>> 0;
}

return [hash[0], hash[1]].map((value) => value.toString(16).padStart(8, '0')).join('');
};

const isUsableSourceCandidate = (value: string): boolean => {
const trimmed = value.trim();
if (!trimmed) return false;
const lowered = trimmed.toLowerCase();
if (lowered === 'file' || lowered === 'memory') return false;
return lowered.endsWith('.json') || trimmed.includes('/') || trimmed.includes('\\');
};

const buildLegacyConfigSeed = (input: {
providerKey: string;
compatName?: string;
baseURL?: string;
proxyURL?: string;
apiKey?: string;
source?: string;
}) => {
const providerKey = input.providerKey.trim().toLowerCase();
if (!providerKey) return '';

const parts = [`provider=${providerKey}`];
if (input.compatName) parts.push(`compat=${input.compatName.trim().toLowerCase()}`);
if (input.baseURL) parts.push(`base=${input.baseURL.trim()}`);
if (input.proxyURL) parts.push(`proxy=${input.proxyURL.trim()}`);
if (input.apiKey) parts.push(`api_key=${input.apiKey.trim()}`);
if (input.source) parts.push(`source=${input.source.trim()}`);

return parts.length > 1 ? `config:${parts.join('\x00')}` : '';
};

export const buildLegacyAuthIndexAliases = (entry: AuthFileItem): string[] => {
const record = entry as RecordLike;
const seeds = new Set<string>();
const name = readAnyString(record, ['name']);
const id = readAnyString(record, ['id']);
const providerKey = readAnyString(record, ['provider_key', 'providerKey', 'provider', 'type']);
const compatName = readAnyString(record, ['compat_name', 'compatName']);
const baseURL = readAnyString(record, ['base_url', 'baseUrl', 'base-url']);
const proxyURL = readAnyString(record, ['proxy_url', 'proxyUrl', 'proxy-url']);
const apiKey = readAnyString(record, ['api_key', 'apiKey', 'api-key']);

[name, id].forEach((value) => {
if (value) seeds.add(`file:${value}`);
});
if (id) seeds.add(`id:${id}`);

const sourceCandidates = [
readAnyString(record, ['path']),
readAnyString(record, ['source']),
readAnyString(record, ['file']),
].filter(isUsableSourceCandidate);

sourceCandidates.forEach((source) => {
const seed = buildLegacyConfigSeed({
providerKey,
compatName,
baseURL,
proxyURL,
apiKey,
source,
});
if (seed) seeds.add(seed);
});

if (apiKey || baseURL || proxyURL || compatName) {
const seed = buildLegacyConfigSeed({
providerKey,
compatName,
baseURL,
proxyURL,
apiKey,
});
if (seed) seeds.add(seed);
}

return Array.from(seeds)
.map(stableAuthIndexFromSeed)
.filter(Boolean);
};
Loading
Loading