11import { useCallback , useEffect , useState } from 'react' ;
22import { useInterval } from '@/hooks/useInterval' ;
3- import { apiKeyUsageApi } from '@/services/api' ;
3+ import { apiKeyUsageApi , usageApi } from '@/services/api' ;
4+ import { normalizeAuthIndex , normalizeUsageSourceId } from '@/utils/usage' ;
45import {
56 normalizeRecentRequestUsageEntry ,
67 type ApiKeyUsageResponse ,
8+ type RecentRequestBucket ,
79 type RecentRequestUsageEntry ,
810} from '@/utils/recentRequests' ;
911
1012const PROVIDER_RECENT_REQUESTS_STALE_TIME_MS = 240_000 ;
13+ const USAGE_FALLBACK_PROVIDER_KEY = '__usage_fallback__' ;
14+ const RECENT_REQUEST_BLOCK_COUNT = 20 ;
15+ const RECENT_REQUEST_BLOCK_DURATION_MS = 10 * 60 * 1000 ;
1116
1217export type ProviderRecentRequests = Map < string , Map < string , RecentRequestUsageEntry > > ;
1318
@@ -26,6 +31,30 @@ const normalizeProviderKey = (value: unknown): string =>
2631 . trim ( )
2732 . toLowerCase ( ) ;
2833
34+ const isRecord = ( value : unknown ) : value is Record < string , unknown > =>
35+ Boolean ( value && typeof value === 'object' && ! Array . isArray ( value ) ) ;
36+
37+ const parseTimestampMs = ( value : unknown ) : number | null => {
38+ if ( typeof value !== 'string' || ! value . trim ( ) ) return null ;
39+ const parsed = Date . parse ( value ) ;
40+ return Number . isFinite ( parsed ) ? parsed : null ;
41+ } ;
42+
43+ const createEmptyRecentBuckets = ( now : number ) : RecentRequestBucket [ ] => {
44+ const windowStart = now - RECENT_REQUEST_BLOCK_COUNT * RECENT_REQUEST_BLOCK_DURATION_MS ;
45+ return Array . from ( { length : RECENT_REQUEST_BLOCK_COUNT } , ( _ , index ) => ( {
46+ time : new Date ( windowStart + index * RECENT_REQUEST_BLOCK_DURATION_MS ) . toISOString ( ) ,
47+ success : 0 ,
48+ failed : 0 ,
49+ } ) ) ;
50+ } ;
51+
52+ const getUsageSnapshotRoot = ( payload : unknown ) : Record < string , unknown > | null => {
53+ const record = isRecord ( payload ) ? payload : null ;
54+ if ( ! record ) return null ;
55+ return isRecord ( record . usage ) ? record . usage : record ;
56+ } ;
57+
2958const normalizeApiKeyUsageResponse = ( payload : ApiKeyUsageResponse ) : ProviderRecentRequests => {
3059 if ( ! payload || typeof payload !== 'object' || Array . isArray ( payload ) ) {
3160 return EMPTY_USAGE_BY_PROVIDER ;
@@ -50,15 +79,134 @@ const normalizeApiKeyUsageResponse = (payload: ApiKeyUsageResponse): ProviderRec
5079 return usageByProvider ;
5180} ;
5281
82+ const ensureUsageFallbackEntry = (
83+ usageByCompositeKey : Map < string , RecentRequestUsageEntry > ,
84+ compositeKey : string ,
85+ now : number
86+ ) : RecentRequestUsageEntry => {
87+ const existing = usageByCompositeKey . get ( compositeKey ) ;
88+ if ( existing ) return existing ;
89+ const created : RecentRequestUsageEntry = {
90+ success : 0 ,
91+ failed : 0 ,
92+ recentRequests : createEmptyRecentBuckets ( now ) ,
93+ } ;
94+ usageByCompositeKey . set ( compositeKey , created ) ;
95+ return created ;
96+ } ;
97+
98+ const addUsageFallbackDetail = (
99+ usageByCompositeKey : Map < string , RecentRequestUsageEntry > ,
100+ compositeKey : string ,
101+ failed : boolean ,
102+ timestampMs : number | null ,
103+ now : number
104+ ) => {
105+ const entry = ensureUsageFallbackEntry ( usageByCompositeKey , compositeKey , now ) ;
106+ if ( failed ) {
107+ entry . failed += 1 ;
108+ } else {
109+ entry . success += 1 ;
110+ }
111+
112+ if ( timestampMs === null ) return ;
113+ const windowStart = now - RECENT_REQUEST_BLOCK_COUNT * RECENT_REQUEST_BLOCK_DURATION_MS ;
114+ const bucketIndex = Math . floor ( ( timestampMs - windowStart ) / RECENT_REQUEST_BLOCK_DURATION_MS ) ;
115+ if ( bucketIndex < 0 || bucketIndex >= RECENT_REQUEST_BLOCK_COUNT ) return ;
116+
117+ const bucket = entry . recentRequests [ bucketIndex ] ;
118+ if ( ! bucket ) return ;
119+ if ( failed ) {
120+ bucket . failed += 1 ;
121+ } else {
122+ bucket . success += 1 ;
123+ }
124+ } ;
125+
126+ const normalizeUsageFallbackResponse = (
127+ payload : unknown ,
128+ now = Date . now ( )
129+ ) : ProviderRecentRequests => {
130+ const usageRoot = getUsageSnapshotRoot ( payload ) ;
131+ const apis = isRecord ( usageRoot ?. apis ) ? usageRoot . apis : null ;
132+ if ( ! apis ) return EMPTY_USAGE_BY_PROVIDER ;
133+
134+ const usageByCompositeKey = new Map < string , RecentRequestUsageEntry > ( ) ;
135+
136+ Object . values ( apis ) . forEach ( ( apiEntry ) => {
137+ if ( ! isRecord ( apiEntry ) || ! isRecord ( apiEntry . models ) ) return ;
138+
139+ Object . values ( apiEntry . models ) . forEach ( ( modelEntry ) => {
140+ if ( ! isRecord ( modelEntry ) || ! Array . isArray ( modelEntry . details ) ) return ;
141+
142+ modelEntry . details . forEach ( ( detail ) => {
143+ if ( ! isRecord ( detail ) ) return ;
144+ const candidateKeys = new Set < string > ( ) ;
145+ const sourceKey = normalizeUsageSourceId ( detail . source ) ;
146+ const authIndexKey = normalizeAuthIndex ( detail . auth_index ) ;
147+ if ( sourceKey ) candidateKeys . add ( sourceKey ) ;
148+ if ( authIndexKey ) candidateKeys . add ( authIndexKey ) ;
149+ if ( candidateKeys . size === 0 ) return ;
150+
151+ const failed = detail . failed === true ;
152+ const timestampMs = parseTimestampMs ( detail . timestamp ) ;
153+ candidateKeys . forEach ( ( candidateKey ) =>
154+ addUsageFallbackDetail ( usageByCompositeKey , candidateKey , failed , timestampMs , now )
155+ ) ;
156+ } ) ;
157+ } ) ;
158+ } ) ;
159+
160+ if ( usageByCompositeKey . size === 0 ) {
161+ return EMPTY_USAGE_BY_PROVIDER ;
162+ }
163+
164+ const usageByProvider : ProviderRecentRequests = new Map ( ) ;
165+ usageByProvider . set ( USAGE_FALLBACK_PROVIDER_KEY , usageByCompositeKey ) ;
166+ return usageByProvider ;
167+ } ;
168+
169+ const mergeProviderRecentRequests = (
170+ primary : ProviderRecentRequests ,
171+ fallback : ProviderRecentRequests
172+ ) : ProviderRecentRequests => {
173+ if ( fallback . size === 0 ) return primary ;
174+ if ( primary . size === 0 ) return fallback ;
175+
176+ const merged : ProviderRecentRequests = new Map ( primary ) ;
177+ fallback . forEach ( ( fallbackEntries , provider ) => {
178+ const providerKey = normalizeProviderKey ( provider ) ;
179+ const nextEntries = new Map ( merged . get ( providerKey ) ?? [ ] ) ;
180+ fallbackEntries . forEach ( ( entry , compositeKey ) => {
181+ if ( ! nextEntries . has ( compositeKey ) ) {
182+ nextEntries . set ( compositeKey , entry ) ;
183+ }
184+ } ) ;
185+ merged . set ( providerKey , nextEntries ) ;
186+ } ) ;
187+ return merged ;
188+ } ;
189+
53190const fetchProviderRecentRequests = async ( ) : Promise < ProviderRecentRequests > => {
54191 if ( ! inFlightRequest ) {
55- inFlightRequest = apiKeyUsageApi
56- . getUsage ( )
57- . then ( ( payload ) => {
58- const normalized = normalizeApiKeyUsageResponse ( payload ) ;
59- cachedUsageByProvider = normalized ;
192+ inFlightRequest = Promise . allSettled ( [ apiKeyUsageApi . getUsage ( ) , usageApi . getUsage ( ) ] )
193+ . then ( ( [ apiKeyUsageResult , usageResult ] ) => {
194+ if ( apiKeyUsageResult . status === 'rejected' && usageResult . status === 'rejected' ) {
195+ throw apiKeyUsageResult . reason ;
196+ }
197+ const primary =
198+ apiKeyUsageResult . status === 'fulfilled'
199+ ? normalizeApiKeyUsageResponse ( apiKeyUsageResult . value )
200+ : EMPTY_USAGE_BY_PROVIDER ;
201+ // `/api-key-usage` 是主数据源;`/usage` 只作为历史/裁剪场景的补洞来源。
202+ const fallback =
203+ usageResult . status === 'fulfilled'
204+ ? normalizeUsageFallbackResponse ( usageResult . value )
205+ : EMPTY_USAGE_BY_PROVIDER ;
206+ const merged = mergeProviderRecentRequests ( primary , fallback ) ;
207+ cachedUsageByProvider = merged ;
60208 cachedAt = Date . now ( ) ;
61- return normalized ;
209+ return merged ;
62210 } )
63211 . finally ( ( ) => {
64212 inFlightRequest = null ;
0 commit comments