@@ -2,6 +2,7 @@ import { Fragment, useMemo } from 'react';
22import { useTranslation } from 'react-i18next' ;
33import { Button } from '@/components/ui/Button' ;
44import { Card } from '@/components/ui/Card' ;
5+ import { IconCheck , IconX } from '@/components/ui/icons' ;
56import { ToggleSwitch } from '@/components/ui/ToggleSwitch' ;
67import iconCodex from '@/assets/icons/codex.svg' ;
78import type { ProviderKeyConfig } from '@/types' ;
@@ -18,7 +19,13 @@ import {
1819import styles from '@/pages/AiProvidersPage.module.scss' ;
1920import { ProviderList } from '../ProviderList' ;
2021import { ProviderStatusBar } from '../ProviderStatusBar' ;
21- import { getStatsBySource , hasDisableAllModelsRule } from '../utils' ;
22+ import {
23+ getApiKeyEntriesStats ,
24+ getProviderApiKeyEntries ,
25+ getProviderPrimaryApiKey ,
26+ getStatsBySource ,
27+ hasDisableAllModelsRule ,
28+ } from '../utils' ;
2229
2330interface CodexSectionProps {
2431 configs : ProviderKeyConfig [ ] ;
@@ -52,15 +59,15 @@ export function CodexSection({
5259 const statusBarCache = useMemo ( ( ) => {
5360 const cache = new Map < string , ReturnType < typeof calculateStatusBarData > > ( ) ;
5461
55- configs . forEach ( ( config ) => {
56- if ( ! config . apiKey ) return ;
57- const candidates = buildCandidateUsageSourceIds ( {
58- apiKey : config . apiKey ,
59- prefix : config . prefix ,
62+ configs . forEach ( ( config , index ) => {
63+ const candidates = new Set < string > ( ) ;
64+ buildCandidateUsageSourceIds ( { prefix : config . prefix } ) . forEach ( ( id ) => candidates . add ( id ) ) ;
65+ getProviderApiKeyEntries ( config ) . forEach ( ( entry ) => {
66+ buildCandidateUsageSourceIds ( { apiKey : entry . apiKey } ) . forEach ( ( id ) => candidates . add ( id ) ) ;
6067 } ) ;
61- if ( ! candidates . length ) return ;
68+ if ( ! candidates . size ) return ;
6269 cache . set (
63- config . apiKey ,
70+ ` ${ getProviderPrimaryApiKey ( config ) } : ${ index } ` ,
6471 calculateStatusBarData ( collectUsageDetailsForCandidates ( usageDetailsBySource , candidates ) )
6572 ) ;
6673 } ) ;
@@ -86,7 +93,7 @@ export function CodexSection({
8693 < ProviderList < ProviderKeyConfig >
8794 items = { configs }
8895 loading = { loading }
89- keyField = { ( item ) => item . apiKey }
96+ keyField = { ( item , index ) => ` ${ getProviderPrimaryApiKey ( item ) || 'codex' } : ${ index } ` }
9097 emptyTitle = { t ( 'ai_providers.codex_empty_title' ) }
9198 emptyDescription = { t ( 'ai_providers.codex_empty_desc' ) }
9299 onEdit = { onEdit }
@@ -101,19 +108,25 @@ export function CodexSection({
101108 onChange = { ( value ) => void onToggle ( index , value ) }
102109 />
103110 ) }
104- renderContent = { ( item ) => {
105- const stats = getStatsBySource ( item . apiKey , keyStats , item . prefix ) ;
111+ renderContent = { ( item , index ) => {
112+ const apiKeyEntries = getProviderApiKeyEntries ( item ) ;
113+ const stats = getApiKeyEntriesStats ( apiKeyEntries , keyStats , item . prefix ) ;
106114 const headerEntries = Object . entries ( item . headers || { } ) ;
107115 const configDisabled = hasDisableAllModelsRule ( item . excludedModels ) ;
108116 const excludedModels = item . excludedModels ?? [ ] ;
109- const statusData = statusBarCache . get ( item . apiKey ) || calculateStatusBarData ( [ ] ) ;
117+ const statusData =
118+ statusBarCache . get ( `${ getProviderPrimaryApiKey ( item ) || 'codex' } :${ index } ` ) ||
119+ calculateStatusBarData ( [ ] ) ;
110120
111121 return (
112122 < Fragment >
113- < div className = "item-title" > { t ( 'ai_providers.codex_item_title' ) } </ div >
114- < div className = { styles . fieldRow } >
115- < span className = { styles . fieldLabel } > { t ( 'common.api_key' ) } :</ span >
116- < span className = { styles . fieldValue } > { maskApiKey ( item . apiKey ) } </ span >
123+ < div className = "item-title" >
124+ { t ( 'ai_providers.codex_item_title' ) }
125+ { configDisabled && (
126+ < span className = "status-badge warning" >
127+ { t ( 'ai_providers.config_disabled_badge' ) }
128+ </ span >
129+ ) }
117130 </ div >
118131 { item . priority !== undefined && (
119132 < div className = { styles . fieldRow } >
@@ -133,18 +146,45 @@ export function CodexSection({
133146 < span className = { styles . fieldValue } > { item . baseUrl } </ span >
134147 </ div >
135148 ) }
136- { item . proxyUrl && (
137- < div className = { styles . fieldRow } >
138- < span className = { styles . fieldLabel } > { t ( 'common.proxy_url' ) } :</ span >
139- < span className = { styles . fieldValue } > { item . proxyUrl } </ span >
140- </ div >
141- ) }
142149 { item . websockets !== undefined && (
143150 < div className = { styles . fieldRow } >
144151 < span className = { styles . fieldLabel } > { t ( 'ai_providers.codex_websockets_label' ) } :</ span >
145152 < span className = { styles . fieldValue } > { item . websockets ? t ( 'common.yes' ) : t ( 'common.no' ) } </ span >
146153 </ div >
147154 ) }
155+ { apiKeyEntries . length > 0 && (
156+ < div className = { styles . apiKeyEntriesSection } >
157+ < div className = { styles . apiKeyEntriesLabel } >
158+ { t ( 'ai_providers.codex_keys_count' ) } : { apiKeyEntries . length }
159+ </ div >
160+ < div className = { styles . apiKeyEntryList } >
161+ { apiKeyEntries . map ( ( entry , entryIndex ) => {
162+ const entryStats = getStatsBySource ( entry . apiKey , keyStats ) ;
163+ return (
164+ < div key = { entryIndex } className = { styles . apiKeyEntryCard } >
165+ < span className = { styles . apiKeyEntryIndex } > { entryIndex + 1 } </ span >
166+ < span className = { styles . apiKeyEntryKey } > { maskApiKey ( entry . apiKey ) } </ span >
167+ { entry . proxyUrl && (
168+ < span className = { styles . apiKeyEntryProxy } > { entry . proxyUrl } </ span >
169+ ) }
170+ < div className = { styles . apiKeyEntryStats } >
171+ < span
172+ className = { `${ styles . apiKeyEntryStat } ${ styles . apiKeyEntryStatSuccess } ` }
173+ >
174+ < IconCheck size = { 12 } /> { entryStats . success }
175+ </ span >
176+ < span
177+ className = { `${ styles . apiKeyEntryStat } ${ styles . apiKeyEntryStatFailure } ` }
178+ >
179+ < IconX size = { 12 } /> { entryStats . failure }
180+ </ span >
181+ </ div >
182+ </ div >
183+ ) ;
184+ } ) }
185+ </ div >
186+ </ div >
187+ ) }
148188 { headerEntries . length > 0 && (
149189 < div className = { styles . headerBadgeList } >
150190 { headerEntries . map ( ( [ key , value ] ) => (
@@ -154,11 +194,6 @@ export function CodexSection({
154194 ) ) }
155195 </ div >
156196 ) }
157- { configDisabled && (
158- < div className = "status-badge warning" style = { { marginTop : 8 , marginBottom : 0 } } >
159- { t ( 'ai_providers.config_disabled_badge' ) }
160- </ div >
161- ) }
162197 { item . models ?. length ? (
163198 < div className = { styles . modelTagList } >
164199 < span className = { styles . modelCountLabel } >
0 commit comments