diff --git a/apps/web/src/features/demo/demoFixtures.ts b/apps/web/src/features/demo/demoFixtures.ts index 4e2c5c7d9..21e821522 100644 --- a/apps/web/src/features/demo/demoFixtures.ts +++ b/apps/web/src/features/demo/demoFixtures.ts @@ -236,7 +236,9 @@ const initialRawConfig: Record = { name: 'OpenAI Compatible', prefix: 'openai', 'base-url': 'https://api.openai.example/v1', - 'api-key-entries': [{ 'api-key': 'sk-compatible-demo-primary' }], + 'api-key-entries': [ + { 'api-key': 'sk-compatible-demo-primary', 'auth-index': 'openai-primary' }, + ], models: [ { name: 'gpt-4.1', alias: 'GPT-4.1' }, { name: 'gpt-4.1-mini', alias: 'GPT-4.1 Mini' }, @@ -244,11 +246,38 @@ const initialRawConfig: Record = { priority: 50, 'test-model': 'gpt-4.1-mini', }, + { + // Multi-key OpenAI-compatible provider: monitoring should show "kuaileshifu #1/#2". + name: 'kuaileshifu', + 'base-url': 'https://api.kuaileshifu.example/v1', + 'api-key-entries': [ + { 'api-key': 'sk-kuai-demo-key-1111aaaa', 'auth-index': 'kuai-auth-1' }, + { 'api-key': 'sk-kuai-demo-key-2222bbbb', 'auth-index': 'kuai-auth-2' }, + ], + models: [ + { name: 'gpt-4.1-mini', alias: 'Kuai Mini' }, + { name: 'gpt-4.1', alias: 'Kuai Full' }, + ], + priority: 55, + 'test-model': 'gpt-4.1-mini', + }, + { + // Named channel that already includes an ordinal (not multi-key disambiguation). + name: 'anyrouter.top #1', + 'base-url': 'https://anyrouter.top/v1', + 'api-key-entries': [ + { 'api-key': 'sk-anyrouter-demo-key', 'auth-index': 'anyrouter-auth-1' }, + ], + models: [{ name: 'gpt-4.1-mini', alias: 'AnyRouter Mini' }], + priority: 60, + }, { name: 'Automation Shared Pool', prefix: 'auto', 'base-url': 'https://gateway.example.com/v1', - 'api-key-entries': [{ 'api-key': 'sk-automation-demo' }], + 'api-key-entries': [ + { 'api-key': 'sk-automation-demo', 'auth-index': 'openai-automation-01' }, + ], models: [{ name: 'qwen-plus', alias: 'Qwen Plus' }], priority: 70, }, @@ -260,7 +289,7 @@ const initialRawConfig: Record = { }; const demoAuthFiles: AuthFilesResponse = { - total: 13, + total: 18, files: [ { name: 'codex-team-01.json', @@ -278,6 +307,26 @@ const demoAuthFiles: AuthFilesResponse = { success: 1842, failed: 18, }, + { + // Codex OAuth-style email identity: primary should be the email, secondary "codex". + name: 'codex-email-user.json', + type: 'codex', + provider: 'codex', + authIndex: 'codex-email-user-01', + disabled: false, + status: 'healthy', + statusMessage: 'Ready', + size: 4680, + modified: now() - 90 * minute, + account_snapshot: 'fbcabcdef@vip.qq.com', + email: 'fbcabcdef@vip.qq.com', + account: 'fbcabcdef@vip.qq.com', + label: 'codex', + account_id: 'acct_codex_email', + plan_type: 'plus', + success: 640, + failed: 6, + }, { name: 'codex-pro-20x-01.json', type: 'codex', @@ -376,6 +425,7 @@ const demoAuthFiles: AuthFilesResponse = { failed: 36, }, { + // xAI OAuth-style email identity: primary should be the email, secondary "xai". name: 'xai-ops.json', type: 'xai', provider: 'xai', @@ -384,9 +434,29 @@ const demoAuthFiles: AuthFilesResponse = { status: 'healthy', size: 3180, modified: now() - day, + account_snapshot: 'oc0demo01@yijihwjw.com', + email: 'oc0demo01@yijihwjw.com', + account: 'oc0demo01@yijihwjw.com', + label: 'xai', success: 294, failed: 4, }, + { + name: 'xai-email-user.json', + type: 'xai', + provider: 'xai', + authIndex: 'xai-email-user-01', + disabled: false, + status: 'healthy', + size: 3020, + modified: now() - 5 * hour, + account_snapshot: 'oc1demo02@yijihwjw.com', + email: 'oc1demo02@yijihwjw.com', + account: 'oc1demo02@yijihwjw.com', + label: 'xai', + success: 188, + failed: 3, + }, { name: 'openai-support-02.json', type: 'openai', @@ -441,6 +511,48 @@ const demoAuthFiles: AuthFilesResponse = { success: 312, failed: 16, }, + { + name: 'kuai-auth-1.json', + type: 'openai', + provider: 'openai', + authIndex: 'kuai-auth-1', + disabled: false, + status: 'healthy', + size: 2680, + modified: now() - 2 * hour, + account_snapshot: 'kuaileshifu', + label: 'kuaileshifu', + success: 420, + failed: 5, + }, + { + name: 'kuai-auth-2.json', + type: 'openai', + provider: 'openai', + authIndex: 'kuai-auth-2', + disabled: false, + status: 'healthy', + size: 2680, + modified: now() - 3 * hour, + account_snapshot: 'kuaileshifu', + label: 'kuaileshifu', + success: 360, + failed: 4, + }, + { + name: 'anyrouter-auth-1.json', + type: 'openai', + provider: 'openai', + authIndex: 'anyrouter-auth-1', + disabled: false, + status: 'healthy', + size: 2540, + modified: now() - 4 * hour, + account_snapshot: 'anyrouter.top #1', + label: 'anyrouter.top #1', + success: 280, + failed: 3, + }, ], }; @@ -664,6 +776,11 @@ const demoApiAliases: ApiKeyAlias[] = [ { apiKeyHash: 'hash_kimi_coding', alias: 'Kimi Coding', updatedAtMs: now() - 9 * hour }, { apiKeyHash: 'hash_builder_lab', alias: 'Builder Lab', updatedAtMs: now() - 10 * hour }, { apiKeyHash: 'hash_xai_ops', alias: 'xAI Ops', updatedAtMs: now() - 11 * hour }, + { apiKeyHash: 'hash_xai_email_user', alias: 'xAI Email User', updatedAtMs: now() - 9 * hour }, + { apiKeyHash: 'hash_codex_email_user', alias: 'Codex Email User', updatedAtMs: now() - 8 * hour }, + { apiKeyHash: 'hash_kuai_key_1', alias: 'kuaileshifu #1', updatedAtMs: now() - 6 * hour }, + { apiKeyHash: 'hash_kuai_key_2', alias: 'kuaileshifu #2', updatedAtMs: now() - 5 * hour }, + { apiKeyHash: 'hash_anyrouter_top', alias: 'anyrouter.top #1', updatedAtMs: now() - 4 * hour }, { apiKeyHash: 'hash_deepseek_ops', alias: 'DeepSeek Ops', updatedAtMs: now() - 12 * hour }, ]; @@ -1276,9 +1393,10 @@ const buildMonitoringAnalytics = ( ], }, { + // xAI email identity: primary masked email, secondary "xai". id: 'acct_ops_console', - account_snapshot: 'Ops Console', - auth_label_snapshot: 'xAI Ops', + account_snapshot: 'oc0demo01@yijihwjw.com', + auth_label_snapshot: 'xai', auth_provider_snapshot: 'xai', auth_indices: ['xai-ops-01'], sources: ['ops'], @@ -1293,6 +1411,103 @@ const buildMonitoringAnalytics = ( buildNestedModelRow('grok-4-fast', 860, 14, 690_000, 12.2, analyticsNow - 55 * minute), ], }, + { + id: 'acct_xai_email_user', + account_snapshot: 'oc1demo02@yijihwjw.com', + auth_label_snapshot: 'xai', + auth_provider_snapshot: 'xai', + auth_indices: ['xai-email-user-01'], + sources: ['ops'], + source_hashes: ['src_xai_email_user'], + calls: 520, + failure_calls: 8, + total_tokens: 410_000, + cost: 7.4, + average_latency_ms: 1420, + last_seen_ms: analyticsNow - 14 * minute, + models: [ + buildNestedModelRow('grok-4-fast', 520, 8, 410_000, 7.4, analyticsNow - 14 * minute), + ], + }, + { + // Codex email identity: primary masked email, secondary "codex". + id: 'acct_codex_email_user', + account_snapshot: 'fbcabcdef@vip.qq.com', + auth_label_snapshot: 'codex', + auth_provider_snapshot: 'codex', + auth_indices: ['codex-email-user-01'], + sources: ['team'], + source_hashes: ['src_codex_email_user'], + calls: 980, + failure_calls: 12, + total_tokens: 780_000, + cost: 16.8, + average_latency_ms: 1180, + last_seen_ms: analyticsNow - 9 * minute, + models: [ + buildNestedModelRow('gpt-4.1-mini', 680, 8, 460_000, 6.2, analyticsNow - 9 * minute), + buildNestedModelRow('gpt-4.1', 300, 4, 320_000, 10.6, analyticsNow - 22 * minute), + ], + }, + { + // Multi-key OpenAI-compatible key #1 → primary "kuaileshifu #1". + id: 'acct_kuaileshifu_key_1', + account_snapshot: 'kuaileshifu', + auth_label_snapshot: 'kuaileshifu', + auth_provider_snapshot: 'openai', + auth_indices: ['kuai-auth-1'], + sources: ['k:sk-kuai-demo-key-1111aaaa'], + source_hashes: ['src_kuai_key_1'], + calls: 1240, + failure_calls: 11, + total_tokens: 920_000, + cost: 18.6, + average_latency_ms: 1040, + last_seen_ms: analyticsNow - 5 * minute, + models: [ + buildNestedModelRow('gpt-4.1-mini', 900, 7, 620_000, 9.4, analyticsNow - 5 * minute), + buildNestedModelRow('gpt-4.1', 340, 4, 300_000, 9.2, analyticsNow - 17 * minute), + ], + }, + { + // Multi-key OpenAI-compatible key #2 → primary "kuaileshifu #2". + id: 'acct_kuaileshifu_key_2', + account_snapshot: 'kuaileshifu', + auth_label_snapshot: 'kuaileshifu', + auth_provider_snapshot: 'openai', + auth_indices: ['kuai-auth-2'], + sources: ['k:sk-kuai-demo-key-2222bbbb'], + source_hashes: ['src_kuai_key_2'], + calls: 980, + failure_calls: 9, + total_tokens: 740_000, + cost: 14.2, + average_latency_ms: 1090, + last_seen_ms: analyticsNow - 7 * minute, + models: [ + buildNestedModelRow('gpt-4.1-mini', 720, 6, 510_000, 7.8, analyticsNow - 7 * minute), + buildNestedModelRow('gpt-4.1', 260, 3, 230_000, 6.4, analyticsNow - 25 * minute), + ], + }, + { + // Named channel already containing "#1" (not multi-key disambiguation). + id: 'acct_anyrouter_top', + account_snapshot: 'anyrouter.top #1', + auth_label_snapshot: 'anyrouter.top #1', + auth_provider_snapshot: 'openai', + auth_indices: ['anyrouter-auth-1'], + sources: ['k:sk-anyrouter-demo-key'], + source_hashes: ['src_anyrouter_top'], + calls: 760, + failure_calls: 8, + total_tokens: 560_000, + cost: 9.6, + average_latency_ms: 980, + last_seen_ms: analyticsNow - 12 * minute, + models: [ + buildNestedModelRow('gpt-4.1-mini', 760, 8, 560_000, 9.6, analyticsNow - 12 * minute), + ], + }, { id: 'acct_edge_experiments', account_snapshot: 'Edge Experiments', @@ -1472,8 +1687,8 @@ const buildMonitoringAnalytics = ( auth_index: 'xai-ops-01', source: 'ops', source_hash: 'src_xai_ops', - account_snapshot: 'Ops Console', - auth_label_snapshot: 'xAI Ops', + account_snapshot: 'oc0demo01@yijihwjw.com', + auth_label_snapshot: 'xai', auth_provider_snapshot: 'xai', calls: 860, failure_calls: 14, @@ -1483,6 +1698,91 @@ const buildMonitoringAnalytics = ( last_seen_ms: analyticsNow - 55 * minute, models: accountStats[10].models, }, + { + id: 'xai-email-user-01', + auth_file_snapshot: 'xai-email-user.json', + auth_index: 'xai-email-user-01', + source: 'ops', + source_hash: 'src_xai_email_user', + account_snapshot: 'oc1demo02@yijihwjw.com', + auth_label_snapshot: 'xai', + auth_provider_snapshot: 'xai', + calls: 520, + failure_calls: 8, + total_tokens: 410_000, + cost: 7.4, + average_latency_ms: 1420, + last_seen_ms: analyticsNow - 14 * minute, + models: accountStats[11].models, + }, + { + id: 'codex-email-user-01', + auth_file_snapshot: 'codex-email-user.json', + auth_index: 'codex-email-user-01', + source: 'team', + source_hash: 'src_codex_email_user', + account_snapshot: 'fbcabcdef@vip.qq.com', + auth_label_snapshot: 'codex', + auth_provider_snapshot: 'codex', + calls: 980, + failure_calls: 12, + total_tokens: 780_000, + cost: 16.8, + average_latency_ms: 1180, + last_seen_ms: analyticsNow - 9 * minute, + models: accountStats[12].models, + }, + { + id: 'kuai-auth-1', + auth_file_snapshot: 'kuai-auth-1.json', + auth_index: 'kuai-auth-1', + source: 'k:sk-kuai-demo-key-1111aaaa', + source_hash: 'src_kuai_key_1', + account_snapshot: 'kuaileshifu', + auth_label_snapshot: 'kuaileshifu', + auth_provider_snapshot: 'openai', + calls: 1240, + failure_calls: 11, + total_tokens: 920_000, + cost: 18.6, + average_latency_ms: 1040, + last_seen_ms: analyticsNow - 5 * minute, + models: accountStats[13].models, + }, + { + id: 'kuai-auth-2', + auth_file_snapshot: 'kuai-auth-2.json', + auth_index: 'kuai-auth-2', + source: 'k:sk-kuai-demo-key-2222bbbb', + source_hash: 'src_kuai_key_2', + account_snapshot: 'kuaileshifu', + auth_label_snapshot: 'kuaileshifu', + auth_provider_snapshot: 'openai', + calls: 980, + failure_calls: 9, + total_tokens: 740_000, + cost: 14.2, + average_latency_ms: 1090, + last_seen_ms: analyticsNow - 7 * minute, + models: accountStats[14].models, + }, + { + id: 'anyrouter-auth-1', + auth_file_snapshot: 'anyrouter-auth-1.json', + auth_index: 'anyrouter-auth-1', + source: 'k:sk-anyrouter-demo-key', + source_hash: 'src_anyrouter_top', + account_snapshot: 'anyrouter.top #1', + auth_label_snapshot: 'anyrouter.top #1', + auth_provider_snapshot: 'openai', + calls: 760, + failure_calls: 8, + total_tokens: 560_000, + cost: 9.6, + average_latency_ms: 980, + last_seen_ms: analyticsNow - 12 * minute, + models: accountStats[15].models, + }, { id: 'openai-support-02', auth_file_snapshot: 'openai-support-02.json', @@ -1550,7 +1850,7 @@ const buildMonitoringAnalytics = ( cost: 6.8, average_latency_ms: 1580, last_seen_ms: analyticsNow - 44 * minute, - models: accountStats[11].models, + models: accountStats[16].models, }, ].map((row) => { const tokenSplit = splitTokens(row.total_tokens); @@ -1741,8 +2041,8 @@ const buildMonitoringAnalytics = ( { id: 'hash_xai_ops', api_key_hash: 'hash_xai_ops', - account_snapshot: 'Ops Console', - auth_label_snapshot: 'xAI Ops', + account_snapshot: 'oc0demo01@yijihwjw.com', + auth_label_snapshot: 'xai', auth_provider_snapshot: 'xai', auth_indices: ['xai-ops-01'], sources: ['ops'], @@ -1755,6 +2055,91 @@ const buildMonitoringAnalytics = ( last_seen_ms: analyticsNow - 55 * minute, models: accountStats[10].models, }, + { + id: 'hash_xai_email_user', + api_key_hash: 'hash_xai_email_user', + account_snapshot: 'oc1demo02@yijihwjw.com', + auth_label_snapshot: 'xai', + auth_provider_snapshot: 'xai', + auth_indices: ['xai-email-user-01'], + sources: ['ops'], + source_hashes: ['src_xai_email_user'], + calls: 520, + failure_calls: 8, + total_tokens: 410_000, + cost: 7.4, + average_latency_ms: 1420, + last_seen_ms: analyticsNow - 14 * minute, + models: accountStats[11].models, + }, + { + id: 'hash_codex_email_user', + api_key_hash: 'hash_codex_email_user', + account_snapshot: 'fbcabcdef@vip.qq.com', + auth_label_snapshot: 'codex', + auth_provider_snapshot: 'codex', + auth_indices: ['codex-email-user-01'], + sources: ['team'], + source_hashes: ['src_codex_email_user'], + calls: 980, + failure_calls: 12, + total_tokens: 780_000, + cost: 16.8, + average_latency_ms: 1180, + last_seen_ms: analyticsNow - 9 * minute, + models: accountStats[12].models, + }, + { + id: 'hash_kuai_key_1', + api_key_hash: 'hash_kuai_key_1', + account_snapshot: 'kuaileshifu', + auth_label_snapshot: 'kuaileshifu', + auth_provider_snapshot: 'openai', + auth_indices: ['kuai-auth-1'], + sources: ['k:sk-kuai-demo-key-1111aaaa'], + source_hashes: ['src_kuai_key_1'], + calls: 1240, + failure_calls: 11, + total_tokens: 920_000, + cost: 18.6, + average_latency_ms: 1040, + last_seen_ms: analyticsNow - 5 * minute, + models: accountStats[13].models, + }, + { + id: 'hash_kuai_key_2', + api_key_hash: 'hash_kuai_key_2', + account_snapshot: 'kuaileshifu', + auth_label_snapshot: 'kuaileshifu', + auth_provider_snapshot: 'openai', + auth_indices: ['kuai-auth-2'], + sources: ['k:sk-kuai-demo-key-2222bbbb'], + source_hashes: ['src_kuai_key_2'], + calls: 980, + failure_calls: 9, + total_tokens: 740_000, + cost: 14.2, + average_latency_ms: 1090, + last_seen_ms: analyticsNow - 7 * minute, + models: accountStats[14].models, + }, + { + id: 'hash_anyrouter_top', + api_key_hash: 'hash_anyrouter_top', + account_snapshot: 'anyrouter.top #1', + auth_label_snapshot: 'anyrouter.top #1', + auth_provider_snapshot: 'openai', + auth_indices: ['anyrouter-auth-1'], + sources: ['k:sk-anyrouter-demo-key'], + source_hashes: ['src_anyrouter_top'], + calls: 760, + failure_calls: 8, + total_tokens: 560_000, + cost: 9.6, + average_latency_ms: 980, + last_seen_ms: analyticsNow - 12 * minute, + models: accountStats[15].models, + }, { id: 'hash_deepseek_ops', api_key_hash: 'hash_deepseek_ops', @@ -1770,7 +2155,7 @@ const buildMonitoringAnalytics = ( cost: 6.8, average_latency_ms: 1580, last_seen_ms: analyticsNow - 44 * minute, - models: accountStats[11].models, + models: accountStats[16].models, }, ].map((row) => { const tokenSplit = splitTokens(row.total_tokens); @@ -1960,14 +2345,79 @@ const buildMonitoringAnalytics = ( apiKeyHash: 'hash_xai_ops', authIndex: 'xai-ops-01', authFile: 'xai-ops.json', - account: 'Ops Console', - label: 'xAI Ops', + account: 'oc0demo01@yijihwjw.com', + label: 'xai', provider: 'xai', source: 'ops', sourceHash: 'src_xai_ops', endpoint: '/v1/chat/completions', executor: 'ops', }, + { + model: 'grok-4-fast', + apiKeyHash: 'hash_xai_email_user', + authIndex: 'xai-email-user-01', + authFile: 'xai-email-user.json', + account: 'oc1demo02@yijihwjw.com', + label: 'xai', + provider: 'xai', + source: 'ops', + sourceHash: 'src_xai_email_user', + endpoint: '/v1/chat/completions', + executor: 'ops', + }, + { + model: 'gpt-4.1-mini', + apiKeyHash: 'hash_codex_email_user', + authIndex: 'codex-email-user-01', + authFile: 'codex-email-user.json', + account: 'fbcabcdef@vip.qq.com', + label: 'codex', + provider: 'codex', + source: 'team', + sourceHash: 'src_codex_email_user', + endpoint: '/v1/chat/completions', + executor: 'team', + }, + { + model: 'gpt-4.1-mini', + apiKeyHash: 'hash_kuai_key_1', + authIndex: 'kuai-auth-1', + authFile: 'kuai-auth-1.json', + account: 'kuaileshifu', + label: 'kuaileshifu', + provider: 'openai', + source: 'k:sk-kuai-demo-key-1111aaaa', + sourceHash: 'src_kuai_key_1', + endpoint: '/v1/chat/completions', + executor: 'compat', + }, + { + model: 'gpt-4.1', + apiKeyHash: 'hash_kuai_key_2', + authIndex: 'kuai-auth-2', + authFile: 'kuai-auth-2.json', + account: 'kuaileshifu', + label: 'kuaileshifu', + provider: 'openai', + source: 'k:sk-kuai-demo-key-2222bbbb', + sourceHash: 'src_kuai_key_2', + endpoint: '/v1/chat/completions', + executor: 'compat', + }, + { + model: 'gpt-4.1-mini', + apiKeyHash: 'hash_anyrouter_top', + authIndex: 'anyrouter-auth-1', + authFile: 'anyrouter-auth-1.json', + account: 'anyrouter.top #1', + label: 'anyrouter.top #1', + provider: 'openai', + source: 'k:sk-anyrouter-demo-key', + sourceHash: 'src_anyrouter_top', + endpoint: '/v1/chat/completions', + executor: 'compat', + }, { model: 'deepseek-chat', apiKeyHash: 'hash_deepseek_ops', diff --git a/apps/web/src/features/monitoring/accountOverviewState.test.ts b/apps/web/src/features/monitoring/accountOverviewState.test.ts index 72393608a..0341fc022 100644 --- a/apps/web/src/features/monitoring/accountOverviewState.test.ts +++ b/apps/web/src/features/monitoring/accountOverviewState.test.ts @@ -169,6 +169,25 @@ describe('accountOverviewState', () => { }); }); + it('does not show a bare provider name under its multi-key disambiguated primary label', () => { + const row = createAccountRow({ + account: 'kuaileshifu', + displayAccount: 'kuaileshifu #1', + accountMasked: 'kuaileshifu', + authLabels: ['kuaileshifu'], + channels: ['kuaileshifu'], + }); + + expect(resolveAccountDisplayText(row, 'masked')).toMatchObject({ + primary: 'kuaileshifu #1', + secondary: '', + }); + expect(resolveAccountDisplayText(row, 'full')).toMatchObject({ + primary: 'kuaileshifu #1', + secondary: '', + }); + }); + it('keeps the existing default account sort contract', () => { expect(DEFAULT_ACCOUNT_SORT).toEqual({ key: 'lastSeenAt', direction: 'desc' }); expect(normalizeAccountSortState(undefined)).toEqual(DEFAULT_ACCOUNT_SORT); diff --git a/apps/web/src/features/monitoring/accountOverviewState.ts b/apps/web/src/features/monitoring/accountOverviewState.ts index 085d6619a..9332c51f4 100644 --- a/apps/web/src/features/monitoring/accountOverviewState.ts +++ b/apps/web/src/features/monitoring/accountOverviewState.ts @@ -208,6 +208,21 @@ const hasReadableAccountValue = (value: string | null | undefined) => { const firstReadableAccountValue = (...values: Array) => values.find(hasReadableAccountValue)?.trim() || ''; +const isRedundantAccountLabel = ( + candidate: string | null | undefined, + primary: string | null | undefined +) => { + const candidateValue = String(candidate || '').trim(); + const primaryValue = String(primary || '').trim(); + if (!candidateValue || !primaryValue) return false; + if (candidateValue === primaryValue) return true; + const lowerCandidate = candidateValue.toLowerCase(); + const lowerPrimary = primaryValue.toLowerCase(); + return ( + lowerPrimary.startsWith(`${lowerCandidate} #`) || lowerCandidate.startsWith(`${lowerPrimary} #`) + ); +}; + export const resolveAccountDisplayText = ( row: Pick< MonitoringAccountRow, @@ -236,7 +251,12 @@ export const resolveAccountDisplayText = ( ] : [maskedAccount, fullAccount]; const secondary = - secondaryCandidates.find((value) => hasReadableAccountValue(value) && value !== primary) || ''; + secondaryCandidates.find( + (value) => + hasReadableAccountValue(value) && + value !== primary && + !isRedundantAccountLabel(value, primary) + ) || ''; const titleParts = Array.from( new Set([primary, fullAccount, maskedAccount, secondary].filter(hasReadableAccountValue)) ); diff --git a/apps/web/src/features/monitoring/components/accountOverviewPresentation.ts b/apps/web/src/features/monitoring/components/accountOverviewPresentation.ts index 44b4f4f65..a539d6635 100644 --- a/apps/web/src/features/monitoring/components/accountOverviewPresentation.ts +++ b/apps/web/src/features/monitoring/components/accountOverviewPresentation.ts @@ -142,18 +142,29 @@ export const getCodexPlanLabel = ( return planType || normalized; }; +const isRedundantAccountSecondaryLabel = (candidate: string, primaryText: string) => { + if (!candidate || !primaryText || candidate === primaryText) return true; + const lowerCandidate = candidate.toLowerCase(); + const lowerPrimary = primaryText.toLowerCase(); + return ( + lowerPrimary.startsWith(`${lowerCandidate} #`) || lowerCandidate.startsWith(`${lowerPrimary} #`) + ); +}; + export const buildAccountSecondaryText = (row: MonitoringAccountRow) => { const primaryText = row.displayAccount || row.account; - if (row.account && row.account !== primaryText) { + if (row.account && !isRedundantAccountSecondaryLabel(row.account, primaryText)) { return row.account; } - const extraAuthLabels = row.authLabels.filter((label) => label && label !== primaryText); + const extraAuthLabels = row.authLabels.filter( + (label) => label && !isRedundantAccountSecondaryLabel(label, primaryText) + ); if (extraAuthLabels.length > 0) { return joinShort(extraAuthLabels, 2); } const extraChannels = row.channels.filter( - (label) => label && label !== '-' && label !== primaryText + (label) => label && label !== '-' && !isRedundantAccountSecondaryLabel(label, primaryText) ); if (extraChannels.length > 0) { return joinShort(extraChannels, 2); diff --git a/apps/web/src/features/monitoring/model/eventRows.test.ts b/apps/web/src/features/monitoring/model/eventRows.test.ts index c0aac0256..25fdc5208 100644 --- a/apps/web/src/features/monitoring/model/eventRows.test.ts +++ b/apps/web/src/features/monitoring/model/eventRows.test.ts @@ -220,4 +220,95 @@ describe('buildEventRows', () => { expect(row.provider).toBe('codex'); expect(display.primary).toBe('Shared Relay'); }); + + it('prefers multi-key OpenAI-compatible disambiguation in realtime source cells', () => { + const sourceInfoMap = buildSourceInfoMap({ + openaiCompatibility: [ + { + name: 'kuaileshifu', + baseUrl: 'https://api.kuaileshifu.example/v1', + apiKeyEntries: [ + { apiKey: 'sk-openai111111aaaa', authIndex: 'kuai-auth-1' }, + { apiKey: 'sk-openai222222bbbb', authIndex: 'kuai-auth-2' }, + ], + }, + ], + }); + const authMetaMap = new Map([ + [ + 'kuai-auth-1', + { + authIndex: 'kuai-auth-1', + label: 'kuaileshifu', + account: 'kuaileshifu', + provider: 'openai', + status: 'active', + disabled: false, + unavailable: false, + runtimeOnly: false, + planType: '-', + updatedAt: '', + }, + ], + ]); + const channelByAuthIndex = new Map([ + [ + 'kuai-auth-1', + { + key: 'openai:0', + name: 'kuaileshifu', + baseUrl: 'https://api.kuaileshifu.example/v1', + host: 'api.kuaileshifu.example', + disabled: false, + authIndices: ['kuai-auth-1', 'kuai-auth-2'], + modelNames: [], + }, + ], + ]); + + const [row] = buildEventRows( + [ + { + timestamp: '2026-05-19T10:00:00Z', + source: 'm:sk-o...aaaa', + auth_index: 'kuai-auth-1', + account_snapshot: 'kuaileshifu', + auth_label_snapshot: 'kuaileshifu', + auth_provider_snapshot: 'openai', + latency_ms: 1500, + tokens: { + input_tokens: 10, + output_tokens: 20, + total_tokens: 30, + }, + failed: false, + __modelName: 'gpt-4.1-mini', + __endpoint: 'POST /v1/chat/completions', + __endpointMethod: 'POST', + __endpointPath: '/v1/chat/completions', + __timestampMs: Date.parse('2026-05-19T10:00:00Z'), + }, + ], + authMetaMap, + new Map(), + sourceInfoMap, + channelByAuthIndex, + {}, + new Map() + ); + + const t = ((key: string) => { + if (key === 'monitoring.filter_provider') return 'Provider'; + if (key === 'monitoring.column_host') return 'Host'; + if (key === 'monitoring.source') return 'Source'; + return key; + }) as Parameters[1]; + const display = buildRealtimeSourceDisplay(row, t); + + expect(row.source).toBe('kuaileshifu #1'); + expect(row.sourceMasked).toBe('kuaileshifu #1'); + expect(row.channel).toBe('kuaileshifu'); + expect(display.primary).toBe('kuaileshifu #1'); + expect(display.meta).toBe('Provider: openai'); + }); }); diff --git a/apps/web/src/features/monitoring/model/eventRows.ts b/apps/web/src/features/monitoring/model/eventRows.ts index 36f6d09fc..d0e1df582 100644 --- a/apps/web/src/features/monitoring/model/eventRows.ts +++ b/apps/web/src/features/monitoring/model/eventRows.ts @@ -9,6 +9,7 @@ import { import { formatApiKeyHashLabel } from './base'; import { buildSearchText, maskAuthIndex, maskEmailLike, readString } from './base'; import { sanitizeApiKeyDisplayText, type ApiKeyDisplayInfo } from './apiKeys'; +import { isKeyDisambiguatedLabel } from './sourceDisplay'; import { buildHourLabel, buildLocalDayKey } from './range'; import type { MonitoringAuthMeta, MonitoringChannelMeta, MonitoringEventRow } from './types'; @@ -64,7 +65,28 @@ export const buildEventRows = ( detail.auth_provider_snapshot ?? detail.authProviderSnapshot ); const snapshotDisplay = snapshotAccount || snapshotLabel; - const sourceLabel = authMeta?.label || snapshotDisplay || sourceMeta.displayName || authIndex; + const channelMeta = + channelByAuthIndex.get(authIndex) || + (authMeta?.authIndex ? channelByAuthIndex.get(authMeta.authIndex) : undefined); + const channelLabel = + channelMeta?.name || authMeta?.provider || snapshotProvider || sourceMeta.type || '-'; + const resolvedSourceName = readString(sourceMeta.displayName); + const labelCandidates = authMeta?.label || snapshotLabel || snapshotDisplay; + // Prefer multi-key OpenAI-compatible disambiguation (e.g. "kuaileshifu #1") over the + // bare provider/auth label so realtime cells match account overview identity. + const sourceLabel = + (resolvedSourceName && + (isKeyDisambiguatedLabel(resolvedSourceName, channelMeta?.name) || + isKeyDisambiguatedLabel(resolvedSourceName, channelMeta?.host) || + isKeyDisambiguatedLabel(resolvedSourceName, labelCandidates) || + isKeyDisambiguatedLabel(resolvedSourceName, authMeta?.account) || + isKeyDisambiguatedLabel(resolvedSourceName, snapshotAccount)) + ? resolvedSourceName + : '') || + authMeta?.label || + snapshotDisplay || + resolvedSourceName || + authIndex; const sourceMasked = maskEmailLike(sourceLabel); const account = authMeta?.account || snapshotAccount || sourceLabel; const accountMasked = maskEmailLike(account); @@ -78,11 +100,6 @@ export const buildEventRows = ( apiKeyDisplay?.masked || apiKeyLabel, apiKeyLabel ); - 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; diff --git a/apps/web/src/features/monitoring/model/sourceDisplay.test.ts b/apps/web/src/features/monitoring/model/sourceDisplay.test.ts new file mode 100644 index 000000000..5aeb6f2a5 --- /dev/null +++ b/apps/web/src/features/monitoring/model/sourceDisplay.test.ts @@ -0,0 +1,222 @@ +import { describe, expect, it } from 'vitest'; +import { buildSourceInfoMap } from '@/utils/sourceResolver'; +import { + buildMonitoringSourceDisplay, + isGenericMonitoringProviderLabel, + isKeyDisambiguatedLabel, + isRedundantMonitoringLabel, +} from './sourceDisplay'; +import type { MonitoringAuthMeta, MonitoringChannelMeta } from './types'; + +const emptyContext = { + authMetaMap: new Map(), + channelByAuthIndex: new Map(), +}; + +describe('isGenericMonitoringProviderLabel', () => { + it('treats codex and xAI aliases as generic provider labels', () => { + expect(isGenericMonitoringProviderLabel('codex')).toBe(true); + expect(isGenericMonitoringProviderLabel('xai')).toBe(true); + expect(isGenericMonitoringProviderLabel('XAI')).toBe(true); + expect(isGenericMonitoringProviderLabel('x-ai')).toBe(true); + expect(isGenericMonitoringProviderLabel('grok')).toBe(true); + expect(isGenericMonitoringProviderLabel('anyrouter.top #1')).toBe(false); + }); +}); + +describe('key disambiguation helpers', () => { + it('detects provider/key ordinal disambiguation labels', () => { + expect(isKeyDisambiguatedLabel('kuaileshifu #1', 'kuaileshifu')).toBe(true); + expect(isKeyDisambiguatedLabel('kuaileshifu #2', 'kuaileshifu')).toBe(true); + expect(isKeyDisambiguatedLabel('kuaileshifu', 'kuaileshifu')).toBe(false); + expect(isKeyDisambiguatedLabel('anyrouter.top #1', 'codex')).toBe(false); + expect(isRedundantMonitoringLabel('kuaileshifu', 'kuaileshifu #1')).toBe(true); + }); +}); + +describe('buildMonitoringSourceDisplay', () => { + it('keeps generic xAI provider labels secondary to the account identity', () => { + const authMetaMap = new Map([ + [ + 'xai-1', + { + authIndex: 'xai-1', + label: 'xai', + account: 'oc0abcdef@yijihwjw.com', + provider: 'xai', + status: 'active', + disabled: false, + unavailable: false, + runtimeOnly: false, + planType: '-', + updatedAt: '', + }, + ], + ]); + + const display = buildMonitoringSourceDisplay( + { + authIndex: 'xai-1', + accountSnapshot: 'oc0abcdef@yijihwjw.com', + authLabelSnapshot: 'xai', + authProviderSnapshot: 'xai', + channel: 'xai', + }, + { + authMetaMap, + channelByAuthIndex: new Map(), + } + ); + + expect(display.primary).toBe('oc0***@yijihwjw.com'); + expect(display.meta).toBe('xai'); + expect(display.accountMasked).toBe('oc0***@yijihwjw.com'); + expect(display.provider).toBe('xai'); + }); + + it('keeps generic codex provider labels secondary to the account identity', () => { + const authMetaMap = new Map([ + [ + 'codex-1', + { + authIndex: 'codex-1', + label: 'codex', + account: 'fbcabcdef@vip.qq.com', + provider: 'codex', + status: 'active', + disabled: false, + unavailable: false, + runtimeOnly: false, + planType: '-', + updatedAt: '', + }, + ], + ]); + + const display = buildMonitoringSourceDisplay( + { + authIndex: 'codex-1', + accountSnapshot: 'fbcabcdef@vip.qq.com', + authLabelSnapshot: 'codex', + authProviderSnapshot: 'codex', + channel: 'codex', + }, + { + authMetaMap, + channelByAuthIndex: new Map(), + } + ); + + expect(display.primary).toBe('fbc***@vip.qq.com'); + expect(display.meta).toBe('codex'); + }); + + it('still prefers non-generic channel names over the account identity', () => { + const display = buildMonitoringSourceDisplay( + { + authIndex: 'relay-1', + account: 'user@example.com', + channel: 'anyrouter.top #1', + authProviderSnapshot: 'codex', + }, + emptyContext + ); + + expect(display.primary).toBe('anyrouter.top #1'); + expect(display.meta).toBe('codex'); + }); + + it('prefers OpenAI-compatible multi-key disambiguation over the bare provider name', () => { + const sourceInfoMap = buildSourceInfoMap({ + openaiCompatibility: [ + { + name: 'kuaileshifu', + baseUrl: 'https://api.kuaileshifu.example/v1', + apiKeyEntries: [ + { apiKey: 'sk-openai111111aaaa', authIndex: 'kuai-auth-1' }, + { apiKey: 'sk-openai222222bbbb', authIndex: 'kuai-auth-2' }, + ], + }, + ], + }); + const channelByAuthIndex = new Map([ + [ + 'kuai-auth-1', + { + key: 'openai:0', + name: 'kuaileshifu', + baseUrl: 'https://api.kuaileshifu.example/v1', + host: 'api.kuaileshifu.example', + disabled: false, + authIndices: ['kuai-auth-1', 'kuai-auth-2'], + modelNames: [], + }, + ], + ]); + + const display = buildMonitoringSourceDisplay( + { + authIndex: 'kuai-auth-1', + source: 'm:sk-o...aaaa', + accountSnapshot: 'kuaileshifu', + authLabelSnapshot: 'kuaileshifu', + authProviderSnapshot: 'openai', + channel: 'kuaileshifu', + }, + { + authMetaMap: new Map(), + channelByAuthIndex, + sourceInfoMap, + } + ); + + expect(display.primary).toBe('kuaileshifu #1'); + expect(display.meta).toBe('openai'); + expect(display.channel).toBe('kuaileshifu'); + expect(display.channelHost).toBe('api.kuaileshifu.example'); + }); + + it('keeps a single-key OpenAI-compatible provider name as primary', () => { + const sourceInfoMap = buildSourceInfoMap({ + openaiCompatibility: [ + { + name: 'kuaileshifu', + baseUrl: 'https://api.kuaileshifu.example/v1', + apiKeyEntries: [{ apiKey: 'sk-openai111111aaaa', authIndex: 'kuai-auth-1' }], + }, + ], + }); + const channelByAuthIndex = new Map([ + [ + 'kuai-auth-1', + { + key: 'openai:0', + name: 'kuaileshifu', + baseUrl: 'https://api.kuaileshifu.example/v1', + host: 'api.kuaileshifu.example', + disabled: false, + authIndices: ['kuai-auth-1'], + modelNames: [], + }, + ], + ]); + + const display = buildMonitoringSourceDisplay( + { + authIndex: 'kuai-auth-1', + source: 'm:sk-o...aaaa', + accountSnapshot: 'kuaileshifu', + authProviderSnapshot: 'openai', + channel: 'kuaileshifu', + }, + { + authMetaMap: new Map(), + channelByAuthIndex, + sourceInfoMap, + } + ); + + expect(display.primary).toBe('kuaileshifu'); + expect(display.meta).toBe('openai'); + }); +}); diff --git a/apps/web/src/features/monitoring/model/sourceDisplay.ts b/apps/web/src/features/monitoring/model/sourceDisplay.ts index 25ad8150e..94f3f6650 100644 --- a/apps/web/src/features/monitoring/model/sourceDisplay.ts +++ b/apps/web/src/features/monitoring/model/sourceDisplay.ts @@ -11,6 +11,9 @@ const GENERIC_PROVIDER_LABELS = new Set([ 'gemini', 'claude', 'vertex', + 'xai', + 'x-ai', + 'grok', ]); const hasReadableValue = (value: string | null | undefined) => { @@ -21,6 +24,34 @@ const hasReadableValue = (value: string | null | undefined) => { export const isGenericMonitoringProviderLabel = (value: string) => GENERIC_PROVIDER_LABELS.has(value.trim().toLowerCase()); +/** + * True when `refined` is a key/provider ordinal disambiguation of `base` + * (for example base=`kuaileshifu`, refined=`kuaileshifu #1`). + */ +export const isKeyDisambiguatedLabel = ( + refined: string | null | undefined, + base: string | null | undefined +) => { + const refinedValue = readString(refined); + const baseValue = readString(base); + if (!refinedValue || !baseValue || refinedValue === baseValue) return false; + return refinedValue.toLowerCase().startsWith(`${baseValue.toLowerCase()} #`); +}; + +export const isRedundantMonitoringLabel = ( + candidate: string | null | undefined, + primary: string | null | undefined +) => { + const candidateValue = readString(candidate); + const primaryValue = readString(primary); + if (!candidateValue || !primaryValue) return false; + if (candidateValue === primaryValue) return true; + return ( + isKeyDisambiguatedLabel(primaryValue, candidateValue) || + isKeyDisambiguatedLabel(candidateValue, primaryValue) + ); +}; + const firstReadable = (...values: Array) => values.find(hasReadableValue)?.trim() || ''; @@ -118,24 +149,46 @@ export const buildMonitoringSourceDisplay = ( explicitLabel, snapshotLabel ); - const sourceLabel = firstReadable( - authMeta?.label, - explicitLabel, - snapshotLabel, - account, - sourceMeta.displayName - ); const provider = firstReadable(authMeta?.provider, snapshotProvider, sourceMeta.type); const channel = firstReadable(channelMeta?.name, explicitChannel, provider); const channelHost = firstReadable(channelMeta?.host); + const resolvedSourceName = firstReadable(sourceMeta.displayName); + const labelCandidates = firstReadable(authMeta?.label, explicitLabel, snapshotLabel); + // Prefer key-disambiguated source names (e.g. "kuaileshifu #1") over the bare + // OpenAI-compatible provider/channel name when multi-key providers share a label. + const sourceLabel = firstReadable( + resolvedSourceName && + (isKeyDisambiguatedLabel(resolvedSourceName, channel) || + isKeyDisambiguatedLabel(resolvedSourceName, channelHost) || + isKeyDisambiguatedLabel(resolvedSourceName, labelCandidates) || + isKeyDisambiguatedLabel(resolvedSourceName, account)) + ? resolvedSourceName + : '', + labelCandidates, + account, + resolvedSourceName + ); const sourceMasked = maskEmailLike(sourceLabel || sourceMeta.displayName); const accountMasked = maskEmailLike(account || sourceLabel); const fallbackId = shortHash(input.sourceHash || input.apiKeyHash || authIndex); + const nonGenericChannel = + channel && !isGenericMonitoringProviderLabel(channel) ? channel : ''; + const nonGenericSource = + sourceMasked && !isGenericMonitoringProviderLabel(sourceMasked) ? sourceMasked : ''; + const keyDisambiguatedSource = + nonGenericSource && + (isKeyDisambiguatedLabel(nonGenericSource, channel) || + isKeyDisambiguatedLabel(nonGenericSource, channelHost) || + isKeyDisambiguatedLabel(nonGenericSource, labelCandidates) || + isKeyDisambiguatedLabel(nonGenericSource, account)) + ? nonGenericSource + : ''; const primary = firstReadable( - channel && !isGenericMonitoringProviderLabel(channel) ? channel : '', + keyDisambiguatedSource, + nonGenericChannel, channelHost, - sourceMasked, + nonGenericSource, provider && !isGenericMonitoringProviderLabel(provider) ? provider : '', accountMasked, apiKeyAlias, @@ -144,11 +197,12 @@ export const buildMonitoringSourceDisplay = ( fallbackId ) || '-'; const meta = firstReadable( - provider && provider !== primary ? provider : '', - channelHost && channelHost !== primary ? channelHost : '', - accountMasked && accountMasked !== primary ? accountMasked : '', - sourceMasked && sourceMasked !== primary ? sourceMasked : '', - apiKeyAlias && apiKeyAlias !== primary ? apiKeyAlias : '' + provider && !isRedundantMonitoringLabel(provider, primary) ? provider : '', + channelHost && !isRedundantMonitoringLabel(channelHost, primary) ? channelHost : '', + accountMasked && !isRedundantMonitoringLabel(accountMasked, primary) ? accountMasked : '', + sourceMasked && !isRedundantMonitoringLabel(sourceMasked, primary) ? sourceMasked : '', + apiKeyAlias && !isRedundantMonitoringLabel(apiKeyAlias, primary) ? apiKeyAlias : '', + channel && !isRedundantMonitoringLabel(channel, primary) ? channel : '' ); const title = Array.from( new Set( diff --git a/apps/web/src/features/monitoring/realtimeSourceDisplay.ts b/apps/web/src/features/monitoring/realtimeSourceDisplay.ts index 57c9a6da1..dfd667c75 100644 --- a/apps/web/src/features/monitoring/realtimeSourceDisplay.ts +++ b/apps/web/src/features/monitoring/realtimeSourceDisplay.ts @@ -1,7 +1,11 @@ import type { TFunction } from 'i18next'; import type { MonitoringEventRow } from '@/features/monitoring/hooks/useMonitoringData'; import type { AccountDisplayMode } from '@/features/monitoring/accountOverviewState'; -import { isGenericMonitoringProviderLabel } from '@/features/monitoring/model/sourceDisplay'; +import { + isGenericMonitoringProviderLabel, + isKeyDisambiguatedLabel, + isRedundantMonitoringLabel, +} from '@/features/monitoring/model/sourceDisplay'; const hasReadableRealtimeValue = (value: string | null | undefined) => { const trimmed = String(value || '').trim(); @@ -35,11 +39,22 @@ export const buildRealtimeSourceDisplay = ( const fullSource = firstReadable(row.source, row.account, row.authLabel, row.sourceMasked); const maskedSource = firstReadable(row.sourceMasked, row.accountMasked, row.authLabel, row.source); const source = accountDisplayMode === 'full' ? fullSource : maskedSource; + const nonGenericChannel = + channel && !isGenericMonitoringProviderLabel(channel) ? channel : ''; + const nonGenericSource = source && !isGenericMonitoringProviderLabel(source) ? source : ''; + const keyDisambiguatedSource = + nonGenericSource && + (isKeyDisambiguatedLabel(nonGenericSource, channel) || + isKeyDisambiguatedLabel(nonGenericSource, host) || + isKeyDisambiguatedLabel(nonGenericSource, account)) + ? nonGenericSource + : ''; const primary = firstReadable( - channel && !isGenericMonitoringProviderLabel(channel) ? channel : '', + keyDisambiguatedSource, + nonGenericChannel, host, - source, + nonGenericSource, provider && !isGenericMonitoringProviderLabel(provider) ? provider : '', account || '', channel, @@ -50,7 +65,10 @@ export const buildRealtimeSourceDisplay = ( { value: host, label: t('monitoring.column_host') }, { value: account, label: '' }, { value: source, label: t('monitoring.source') }, - ].find((candidate) => candidate.value && candidate.value !== primary); + ].find( + (candidate) => + candidate.value && !isRedundantMonitoringLabel(candidate.value, primary) + ); const meta = metaCandidate && metaCandidate.label ? `${metaCandidate.label}: ${metaCandidate.value}`