mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-09-24 16:04:35 +08:00
Refactor provider usage tracking to utilize recent requests data
- Introduced `useProviderRecentRequests` hook to manage recent request data for providers. - Replaced usage of `useProviderStats` with `useProviderRecentRequests` in relevant components. - Updated `VertexSection` and other provider components to consume recent request data instead of key stats. - Removed obsolete `useProviderStats` hook and related usage details. - Added new utility functions for handling recent request data, including normalization and aggregation. - Updated API service to fetch recent request usage data. - Refactored `AuthFileCard` and related components to utilize recent request statistics. - Cleaned up unused code and types related to previous usage stats implementation.
This commit is contained in:
@@ -6,23 +6,21 @@ import { ToggleSwitch } from '@/components/ui/ToggleSwitch';
|
||||
import iconClaude from '@/assets/icons/claude.svg';
|
||||
import type { ProviderKeyConfig } from '@/types';
|
||||
import { maskApiKey } from '@/utils/format';
|
||||
import { calculateStatusBarData, type KeyStats } from '@/utils/usage';
|
||||
import { type UsageDetailsByAuthIndex, type UsageDetailsBySource } from '@/utils/usageIndex';
|
||||
import { statusBarDataFromRecentRequests } from '@/utils/recentRequests';
|
||||
import styles from '@/pages/AiProvidersPage.module.scss';
|
||||
import { ProviderList } from '../ProviderList';
|
||||
import { ProviderStatusBar } from '../ProviderStatusBar';
|
||||
import {
|
||||
collectUsageDetailsForIdentity,
|
||||
getProviderConfigKey,
|
||||
getStatsForIdentity,
|
||||
getProviderRecentBuckets,
|
||||
getProviderRecentStats,
|
||||
hasDisableAllModelsRule,
|
||||
type ProviderRecentUsageMap,
|
||||
} from '../utils';
|
||||
|
||||
interface ClaudeSectionProps {
|
||||
configs: ProviderKeyConfig[];
|
||||
keyStats: KeyStats;
|
||||
usageDetailsBySource: UsageDetailsBySource;
|
||||
usageDetailsByAuthIndex: UsageDetailsByAuthIndex;
|
||||
usageByProvider: ProviderRecentUsageMap;
|
||||
loading: boolean;
|
||||
disableControls: boolean;
|
||||
isSwitching: boolean;
|
||||
@@ -34,9 +32,7 @@ interface ClaudeSectionProps {
|
||||
|
||||
export function ClaudeSection({
|
||||
configs,
|
||||
keyStats,
|
||||
usageDetailsBySource,
|
||||
usageDetailsByAuthIndex,
|
||||
usageByProvider,
|
||||
loading,
|
||||
disableControls,
|
||||
isSwitching,
|
||||
@@ -50,25 +46,21 @@ export function ClaudeSection({
|
||||
const toggleDisabled = disableControls || loading || isSwitching;
|
||||
|
||||
const statusBarCache = useMemo(() => {
|
||||
const cache = new Map<string, ReturnType<typeof calculateStatusBarData>>();
|
||||
const cache = new Map<string, ReturnType<typeof statusBarDataFromRecentRequests>>();
|
||||
|
||||
configs.forEach((config, index) => {
|
||||
if (!config.apiKey) return;
|
||||
const configKey = getProviderConfigKey(config, index);
|
||||
cache.set(
|
||||
configKey,
|
||||
calculateStatusBarData(
|
||||
collectUsageDetailsForIdentity(
|
||||
{ authIndex: config.authIndex, apiKey: config.apiKey, prefix: config.prefix },
|
||||
usageDetailsBySource,
|
||||
usageDetailsByAuthIndex
|
||||
)
|
||||
statusBarDataFromRecentRequests(
|
||||
getProviderRecentBuckets(usageByProvider, 'claude', config.apiKey, config.baseUrl)
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
return cache;
|
||||
}, [configs, usageDetailsByAuthIndex, usageDetailsBySource]);
|
||||
}, [configs, usageByProvider]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -104,15 +96,18 @@ export function ClaudeSection({
|
||||
/>
|
||||
)}
|
||||
renderContent={(item, index) => {
|
||||
const stats = getStatsForIdentity(
|
||||
{ authIndex: item.authIndex, apiKey: item.apiKey, prefix: item.prefix },
|
||||
keyStats
|
||||
const stats = getProviderRecentStats(
|
||||
usageByProvider,
|
||||
'claude',
|
||||
item.apiKey,
|
||||
item.baseUrl
|
||||
);
|
||||
const headerEntries = Object.entries(item.headers || {});
|
||||
const configDisabled = hasDisableAllModelsRule(item.excludedModels);
|
||||
const excludedModels = item.excludedModels ?? [];
|
||||
const statusData =
|
||||
statusBarCache.get(getProviderConfigKey(item, index)) || calculateStatusBarData([]);
|
||||
statusBarCache.get(getProviderConfigKey(item, index)) ||
|
||||
statusBarDataFromRecentRequests([]);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
|
||||
@@ -6,23 +6,21 @@ import { ToggleSwitch } from '@/components/ui/ToggleSwitch';
|
||||
import iconCodex from '@/assets/icons/codex.svg';
|
||||
import type { ProviderKeyConfig } from '@/types';
|
||||
import { maskApiKey } from '@/utils/format';
|
||||
import { calculateStatusBarData, type KeyStats } from '@/utils/usage';
|
||||
import { type UsageDetailsByAuthIndex, type UsageDetailsBySource } from '@/utils/usageIndex';
|
||||
import { statusBarDataFromRecentRequests } from '@/utils/recentRequests';
|
||||
import styles from '@/pages/AiProvidersPage.module.scss';
|
||||
import { ProviderList } from '../ProviderList';
|
||||
import { ProviderStatusBar } from '../ProviderStatusBar';
|
||||
import {
|
||||
collectUsageDetailsForIdentity,
|
||||
getProviderConfigKey,
|
||||
getStatsForIdentity,
|
||||
getProviderRecentBuckets,
|
||||
getProviderRecentStats,
|
||||
hasDisableAllModelsRule,
|
||||
type ProviderRecentUsageMap,
|
||||
} from '../utils';
|
||||
|
||||
interface CodexSectionProps {
|
||||
configs: ProviderKeyConfig[];
|
||||
keyStats: KeyStats;
|
||||
usageDetailsBySource: UsageDetailsBySource;
|
||||
usageDetailsByAuthIndex: UsageDetailsByAuthIndex;
|
||||
usageByProvider: ProviderRecentUsageMap;
|
||||
loading: boolean;
|
||||
disableControls: boolean;
|
||||
isSwitching: boolean;
|
||||
@@ -34,9 +32,7 @@ interface CodexSectionProps {
|
||||
|
||||
export function CodexSection({
|
||||
configs,
|
||||
keyStats,
|
||||
usageDetailsBySource,
|
||||
usageDetailsByAuthIndex,
|
||||
usageByProvider,
|
||||
loading,
|
||||
disableControls,
|
||||
isSwitching,
|
||||
@@ -50,25 +46,21 @@ export function CodexSection({
|
||||
const toggleDisabled = disableControls || loading || isSwitching;
|
||||
|
||||
const statusBarCache = useMemo(() => {
|
||||
const cache = new Map<string, ReturnType<typeof calculateStatusBarData>>();
|
||||
const cache = new Map<string, ReturnType<typeof statusBarDataFromRecentRequests>>();
|
||||
|
||||
configs.forEach((config, index) => {
|
||||
if (!config.apiKey) return;
|
||||
const configKey = getProviderConfigKey(config, index);
|
||||
cache.set(
|
||||
configKey,
|
||||
calculateStatusBarData(
|
||||
collectUsageDetailsForIdentity(
|
||||
{ authIndex: config.authIndex, apiKey: config.apiKey, prefix: config.prefix },
|
||||
usageDetailsBySource,
|
||||
usageDetailsByAuthIndex
|
||||
)
|
||||
statusBarDataFromRecentRequests(
|
||||
getProviderRecentBuckets(usageByProvider, 'codex', config.apiKey, config.baseUrl)
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
return cache;
|
||||
}, [configs, usageDetailsByAuthIndex, usageDetailsBySource]);
|
||||
}, [configs, usageByProvider]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -104,15 +96,18 @@ export function CodexSection({
|
||||
/>
|
||||
)}
|
||||
renderContent={(item, index) => {
|
||||
const stats = getStatsForIdentity(
|
||||
{ authIndex: item.authIndex, apiKey: item.apiKey, prefix: item.prefix },
|
||||
keyStats
|
||||
const stats = getProviderRecentStats(
|
||||
usageByProvider,
|
||||
'codex',
|
||||
item.apiKey,
|
||||
item.baseUrl
|
||||
);
|
||||
const headerEntries = Object.entries(item.headers || {});
|
||||
const configDisabled = hasDisableAllModelsRule(item.excludedModels);
|
||||
const excludedModels = item.excludedModels ?? [];
|
||||
const statusData =
|
||||
statusBarCache.get(getProviderConfigKey(item, index)) || calculateStatusBarData([]);
|
||||
statusBarCache.get(getProviderConfigKey(item, index)) ||
|
||||
statusBarDataFromRecentRequests([]);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
|
||||
@@ -6,23 +6,21 @@ import { ToggleSwitch } from '@/components/ui/ToggleSwitch';
|
||||
import iconGemini from '@/assets/icons/gemini.svg';
|
||||
import type { GeminiKeyConfig } from '@/types';
|
||||
import { maskApiKey } from '@/utils/format';
|
||||
import { calculateStatusBarData, type KeyStats } from '@/utils/usage';
|
||||
import { type UsageDetailsByAuthIndex, type UsageDetailsBySource } from '@/utils/usageIndex';
|
||||
import { statusBarDataFromRecentRequests } from '@/utils/recentRequests';
|
||||
import styles from '@/pages/AiProvidersPage.module.scss';
|
||||
import { ProviderList } from '../ProviderList';
|
||||
import { ProviderStatusBar } from '../ProviderStatusBar';
|
||||
import {
|
||||
collectUsageDetailsForIdentity,
|
||||
getProviderConfigKey,
|
||||
getStatsForIdentity,
|
||||
getProviderRecentBuckets,
|
||||
getProviderRecentStats,
|
||||
hasDisableAllModelsRule,
|
||||
type ProviderRecentUsageMap,
|
||||
} from '../utils';
|
||||
|
||||
interface GeminiSectionProps {
|
||||
configs: GeminiKeyConfig[];
|
||||
keyStats: KeyStats;
|
||||
usageDetailsBySource: UsageDetailsBySource;
|
||||
usageDetailsByAuthIndex: UsageDetailsByAuthIndex;
|
||||
usageByProvider: ProviderRecentUsageMap;
|
||||
loading: boolean;
|
||||
disableControls: boolean;
|
||||
isSwitching: boolean;
|
||||
@@ -34,9 +32,7 @@ interface GeminiSectionProps {
|
||||
|
||||
export function GeminiSection({
|
||||
configs,
|
||||
keyStats,
|
||||
usageDetailsBySource,
|
||||
usageDetailsByAuthIndex,
|
||||
usageByProvider,
|
||||
loading,
|
||||
disableControls,
|
||||
isSwitching,
|
||||
@@ -50,25 +46,21 @@ export function GeminiSection({
|
||||
const toggleDisabled = disableControls || loading || isSwitching;
|
||||
|
||||
const statusBarCache = useMemo(() => {
|
||||
const cache = new Map<string, ReturnType<typeof calculateStatusBarData>>();
|
||||
const cache = new Map<string, ReturnType<typeof statusBarDataFromRecentRequests>>();
|
||||
|
||||
configs.forEach((config, index) => {
|
||||
if (!config.apiKey) return;
|
||||
const configKey = getProviderConfigKey(config, index);
|
||||
cache.set(
|
||||
configKey,
|
||||
calculateStatusBarData(
|
||||
collectUsageDetailsForIdentity(
|
||||
{ authIndex: config.authIndex, apiKey: config.apiKey, prefix: config.prefix },
|
||||
usageDetailsBySource,
|
||||
usageDetailsByAuthIndex
|
||||
)
|
||||
statusBarDataFromRecentRequests(
|
||||
getProviderRecentBuckets(usageByProvider, 'gemini', config.apiKey, config.baseUrl)
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
return cache;
|
||||
}, [configs, usageDetailsByAuthIndex, usageDetailsBySource]);
|
||||
}, [configs, usageByProvider]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -104,15 +96,18 @@ export function GeminiSection({
|
||||
/>
|
||||
)}
|
||||
renderContent={(item, index) => {
|
||||
const stats = getStatsForIdentity(
|
||||
{ authIndex: item.authIndex, apiKey: item.apiKey, prefix: item.prefix },
|
||||
keyStats
|
||||
const stats = getProviderRecentStats(
|
||||
usageByProvider,
|
||||
'gemini',
|
||||
item.apiKey,
|
||||
item.baseUrl
|
||||
);
|
||||
const headerEntries = Object.entries(item.headers || {});
|
||||
const configDisabled = hasDisableAllModelsRule(item.excludedModels);
|
||||
const excludedModels = item.excludedModels ?? [];
|
||||
const statusData =
|
||||
statusBarCache.get(getProviderConfigKey(item, index)) || calculateStatusBarData([]);
|
||||
statusBarCache.get(getProviderConfigKey(item, index)) ||
|
||||
statusBarDataFromRecentRequests([]);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
|
||||
@@ -18,16 +18,16 @@ import iconOpenaiLight from '@/assets/icons/openai-light.svg';
|
||||
import iconOpenaiDark from '@/assets/icons/openai-dark.svg';
|
||||
import type { OpenAIProviderConfig } from '@/types';
|
||||
import { maskApiKey } from '@/utils/format';
|
||||
import { calculateStatusBarData, type KeyStats } from '@/utils/usage';
|
||||
import { type UsageDetailsByAuthIndex, type UsageDetailsBySource } from '@/utils/usageIndex';
|
||||
import { statusBarDataFromRecentRequests } from '@/utils/recentRequests';
|
||||
import styles from '@/pages/AiProvidersPage.module.scss';
|
||||
import { ProviderStatusBar } from '../ProviderStatusBar';
|
||||
import { usePageTransitionLayer } from '@/components/common/PageTransitionLayer';
|
||||
import {
|
||||
collectOpenAIProviderUsageDetails,
|
||||
getOpenAIProviderRecentStats,
|
||||
getOpenAIProviderRecentStatusData,
|
||||
getOpenAIProviderKey,
|
||||
getOpenAIProviderStats,
|
||||
getStatsForIdentity,
|
||||
getProviderRecentStats,
|
||||
type ProviderRecentUsageMap,
|
||||
} from '../utils';
|
||||
|
||||
type SortOption = 'name' | 'priority' | 'recent-success';
|
||||
@@ -40,13 +40,11 @@ interface FloatingToolbarStyle {
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
const EMPTY_STATUS_BAR = calculateStatusBarData([]);
|
||||
const EMPTY_STATUS_BAR = statusBarDataFromRecentRequests([]);
|
||||
|
||||
interface OpenAISectionProps {
|
||||
configs: OpenAIProviderConfig[];
|
||||
keyStats: KeyStats;
|
||||
usageDetailsBySource: UsageDetailsBySource;
|
||||
usageDetailsByAuthIndex: UsageDetailsByAuthIndex;
|
||||
usageByProvider: ProviderRecentUsageMap;
|
||||
loading: boolean;
|
||||
disableControls: boolean;
|
||||
isSwitching: boolean;
|
||||
@@ -72,9 +70,7 @@ const getApiKeyEntryRenderKey = (
|
||||
|
||||
export function OpenAISection({
|
||||
configs,
|
||||
keyStats,
|
||||
usageDetailsBySource,
|
||||
usageDetailsByAuthIndex,
|
||||
usageByProvider,
|
||||
loading,
|
||||
disableControls,
|
||||
isSwitching,
|
||||
@@ -253,20 +249,15 @@ export function OpenAISection({
|
||||
: t('ai_providers.model_search_placeholder');
|
||||
|
||||
const statusBarCache = useMemo(() => {
|
||||
const cache = new Map<string, ReturnType<typeof calculateStatusBarData>>();
|
||||
const cache = new Map<string, ReturnType<typeof statusBarDataFromRecentRequests>>();
|
||||
|
||||
configs.forEach((provider, index) => {
|
||||
const providerKey = getOpenAIProviderKey(provider, index);
|
||||
cache.set(
|
||||
providerKey,
|
||||
calculateStatusBarData(
|
||||
collectOpenAIProviderUsageDetails(provider, usageDetailsBySource, usageDetailsByAuthIndex)
|
||||
)
|
||||
);
|
||||
cache.set(providerKey, getOpenAIProviderRecentStatusData(provider, usageByProvider));
|
||||
});
|
||||
|
||||
return cache;
|
||||
}, [configs, usageDetailsByAuthIndex, usageDetailsBySource]);
|
||||
}, [configs, usageByProvider]);
|
||||
|
||||
const sortOptions = useMemo(
|
||||
() => [
|
||||
@@ -288,7 +279,12 @@ export function OpenAISection({
|
||||
const direction = sortDirection === 'desc' ? -1 : 1;
|
||||
const providerStats =
|
||||
sortOption === 'recent-success'
|
||||
? new Map(sorted.map(({ config }) => [config, getOpenAIProviderStats(config, keyStats)]))
|
||||
? new Map(
|
||||
sorted.map(({ config }) => [
|
||||
config,
|
||||
getOpenAIProviderRecentStats(config, usageByProvider),
|
||||
])
|
||||
)
|
||||
: null;
|
||||
|
||||
switch (sortOption) {
|
||||
@@ -326,7 +322,7 @@ export function OpenAISection({
|
||||
}
|
||||
|
||||
return sorted;
|
||||
}, [configs, sortOption, sortDirection, keyStats, selectedModels]);
|
||||
}, [configs, sortOption, sortDirection, usageByProvider, selectedModels]);
|
||||
|
||||
const toggleModelSelection = (modelName: string) => {
|
||||
setSelectedModels((prev) => {
|
||||
@@ -528,7 +524,7 @@ export function OpenAISection({
|
||||
);
|
||||
|
||||
const renderProviderCard = ({ config: provider, originalIndex }: IndexedOpenAIProvider) => {
|
||||
const stats = getOpenAIProviderStats(provider, keyStats);
|
||||
const stats = getOpenAIProviderRecentStats(provider, usageByProvider);
|
||||
const headerEntries = Object.entries(provider.headers || {});
|
||||
const apiKeyEntries = provider.apiKeyEntries || [];
|
||||
const statusData =
|
||||
@@ -580,9 +576,11 @@ export function OpenAISection({
|
||||
</div>
|
||||
<div className={styles.apiKeyEntryList}>
|
||||
{apiKeyEntries.map((entry, entryIndex) => {
|
||||
const entryStats = getStatsForIdentity(
|
||||
{ authIndex: entry.authIndex, apiKey: entry.apiKey },
|
||||
keyStats
|
||||
const entryStats = getProviderRecentStats(
|
||||
usageByProvider,
|
||||
provider.name,
|
||||
entry.apiKey,
|
||||
provider.baseUrl
|
||||
);
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { StatusBarData, StatusBlockDetail } from '@/utils/usage';
|
||||
import type { StatusBarData, StatusBlockDetail } from '@/utils/recentRequests';
|
||||
import defaultStyles from '@/pages/AiProvidersPage.module.scss';
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,23 +6,21 @@ import { ToggleSwitch } from '@/components/ui/ToggleSwitch';
|
||||
import iconVertex from '@/assets/icons/vertex.svg';
|
||||
import type { ProviderKeyConfig } from '@/types';
|
||||
import { maskApiKey } from '@/utils/format';
|
||||
import { calculateStatusBarData, type KeyStats } from '@/utils/usage';
|
||||
import { type UsageDetailsByAuthIndex, type UsageDetailsBySource } from '@/utils/usageIndex';
|
||||
import { statusBarDataFromRecentRequests } from '@/utils/recentRequests';
|
||||
import styles from '@/pages/AiProvidersPage.module.scss';
|
||||
import { ProviderList } from '../ProviderList';
|
||||
import { ProviderStatusBar } from '../ProviderStatusBar';
|
||||
import {
|
||||
collectUsageDetailsForIdentity,
|
||||
getProviderConfigKey,
|
||||
getStatsForIdentity,
|
||||
getProviderRecentBuckets,
|
||||
getProviderRecentStats,
|
||||
hasDisableAllModelsRule,
|
||||
type ProviderRecentUsageMap,
|
||||
} from '../utils';
|
||||
|
||||
interface VertexSectionProps {
|
||||
configs: ProviderKeyConfig[];
|
||||
keyStats: KeyStats;
|
||||
usageDetailsBySource: UsageDetailsBySource;
|
||||
usageDetailsByAuthIndex: UsageDetailsByAuthIndex;
|
||||
usageByProvider: ProviderRecentUsageMap;
|
||||
loading: boolean;
|
||||
disableControls: boolean;
|
||||
isSwitching: boolean;
|
||||
@@ -34,9 +32,7 @@ interface VertexSectionProps {
|
||||
|
||||
export function VertexSection({
|
||||
configs,
|
||||
keyStats,
|
||||
usageDetailsBySource,
|
||||
usageDetailsByAuthIndex,
|
||||
usageByProvider,
|
||||
loading,
|
||||
disableControls,
|
||||
isSwitching,
|
||||
@@ -50,25 +46,21 @@ export function VertexSection({
|
||||
const toggleDisabled = disableControls || loading || isSwitching;
|
||||
|
||||
const statusBarCache = useMemo(() => {
|
||||
const cache = new Map<string, ReturnType<typeof calculateStatusBarData>>();
|
||||
const cache = new Map<string, ReturnType<typeof statusBarDataFromRecentRequests>>();
|
||||
|
||||
configs.forEach((config, index) => {
|
||||
if (!config.apiKey) return;
|
||||
const configKey = getProviderConfigKey(config, index);
|
||||
cache.set(
|
||||
configKey,
|
||||
calculateStatusBarData(
|
||||
collectUsageDetailsForIdentity(
|
||||
{ authIndex: config.authIndex, apiKey: config.apiKey, prefix: config.prefix },
|
||||
usageDetailsBySource,
|
||||
usageDetailsByAuthIndex
|
||||
)
|
||||
statusBarDataFromRecentRequests(
|
||||
getProviderRecentBuckets(usageByProvider, 'vertex', config.apiKey, config.baseUrl)
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
return cache;
|
||||
}, [configs, usageDetailsByAuthIndex, usageDetailsBySource]);
|
||||
}, [configs, usageByProvider]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -104,15 +96,18 @@ export function VertexSection({
|
||||
/>
|
||||
)}
|
||||
renderContent={(item, index) => {
|
||||
const stats = getStatsForIdentity(
|
||||
{ authIndex: item.authIndex, apiKey: item.apiKey, prefix: item.prefix },
|
||||
keyStats
|
||||
const stats = getProviderRecentStats(
|
||||
usageByProvider,
|
||||
'vertex',
|
||||
item.apiKey,
|
||||
item.baseUrl
|
||||
);
|
||||
const headerEntries = Object.entries(item.headers || {});
|
||||
const configDisabled = hasDisableAllModelsRule(item.excludedModels);
|
||||
const excludedModels = item.excludedModels ?? [];
|
||||
const statusData =
|
||||
statusBarCache.get(getProviderConfigKey(item, index)) || calculateStatusBarData([]);
|
||||
statusBarCache.get(getProviderConfigKey(item, index)) ||
|
||||
statusBarDataFromRecentRequests([]);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useInterval } from '@/hooks/useInterval';
|
||||
import { apiKeyUsageApi } from '@/services/api';
|
||||
import {
|
||||
normalizeRecentRequestBuckets,
|
||||
type ApiKeyUsageResponse,
|
||||
type RecentRequestBucket,
|
||||
} from '@/utils/recentRequests';
|
||||
|
||||
const PROVIDER_RECENT_REQUESTS_STALE_TIME_MS = 240_000;
|
||||
|
||||
export type ProviderRecentRequests = Map<string, Map<string, RecentRequestBucket[]>>;
|
||||
|
||||
export type UseProviderRecentRequestsOptions = {
|
||||
enabled?: boolean;
|
||||
};
|
||||
|
||||
const EMPTY_USAGE_BY_PROVIDER: ProviderRecentRequests = new Map();
|
||||
|
||||
let cachedUsageByProvider: ProviderRecentRequests = EMPTY_USAGE_BY_PROVIDER;
|
||||
let cachedAt = 0;
|
||||
let inFlightRequest: Promise<ProviderRecentRequests> | null = null;
|
||||
|
||||
const normalizeProviderKey = (value: unknown): string => String(value ?? '').trim().toLowerCase();
|
||||
|
||||
const normalizeApiKeyUsageResponse = (payload: ApiKeyUsageResponse): ProviderRecentRequests => {
|
||||
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
||||
return EMPTY_USAGE_BY_PROVIDER;
|
||||
}
|
||||
|
||||
const usageByProvider: ProviderRecentRequests = new Map();
|
||||
|
||||
Object.entries(payload).forEach(([provider, entries]) => {
|
||||
const providerKey = normalizeProviderKey(provider);
|
||||
if (!providerKey || !entries || typeof entries !== 'object' || Array.isArray(entries)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const usageByCompositeKey = new Map<string, RecentRequestBucket[]>();
|
||||
Object.entries(entries).forEach(([compositeKey, buckets]) => {
|
||||
usageByCompositeKey.set(compositeKey, normalizeRecentRequestBuckets(buckets));
|
||||
});
|
||||
|
||||
usageByProvider.set(providerKey, usageByCompositeKey);
|
||||
});
|
||||
|
||||
return usageByProvider;
|
||||
};
|
||||
|
||||
const fetchProviderRecentRequests = async (): Promise<ProviderRecentRequests> => {
|
||||
if (!inFlightRequest) {
|
||||
inFlightRequest = apiKeyUsageApi
|
||||
.getUsage()
|
||||
.then((payload) => {
|
||||
const normalized = normalizeApiKeyUsageResponse(payload);
|
||||
cachedUsageByProvider = normalized;
|
||||
cachedAt = Date.now();
|
||||
return normalized;
|
||||
})
|
||||
.finally(() => {
|
||||
inFlightRequest = null;
|
||||
});
|
||||
}
|
||||
|
||||
return inFlightRequest;
|
||||
};
|
||||
|
||||
export function useProviderRecentRequests(options: UseProviderRecentRequestsOptions = {}) {
|
||||
const enabled = options.enabled ?? true;
|
||||
const [usageByProvider, setUsageByProvider] = useState<ProviderRecentRequests>(
|
||||
cachedUsageByProvider
|
||||
);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const loadRecentRequests = useCallback(
|
||||
async (loadOptions: { force?: boolean } = {}) => {
|
||||
if (!enabled) {
|
||||
return EMPTY_USAGE_BY_PROVIDER;
|
||||
}
|
||||
|
||||
const hasFreshCache =
|
||||
cachedAt > 0 &&
|
||||
Date.now() - cachedAt < PROVIDER_RECENT_REQUESTS_STALE_TIME_MS;
|
||||
|
||||
if (!loadOptions.force && hasFreshCache) {
|
||||
setUsageByProvider(cachedUsageByProvider);
|
||||
return cachedUsageByProvider;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const nextUsage = await fetchProviderRecentRequests();
|
||||
setUsageByProvider(nextUsage);
|
||||
return nextUsage;
|
||||
} catch {
|
||||
if (cachedAt > 0) {
|
||||
setUsageByProvider(cachedUsageByProvider);
|
||||
}
|
||||
return cachedUsageByProvider;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
[enabled]
|
||||
);
|
||||
|
||||
const refreshRecentRequests = useCallback(
|
||||
async () => loadRecentRequests({ force: true }),
|
||||
[loadRecentRequests]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setUsageByProvider(enabled ? cachedUsageByProvider : EMPTY_USAGE_BY_PROVIDER);
|
||||
}, [enabled]);
|
||||
|
||||
useInterval(() => {
|
||||
void refreshRecentRequests().catch(() => {});
|
||||
}, enabled ? PROVIDER_RECENT_REQUESTS_STALE_TIME_MS : null);
|
||||
|
||||
return {
|
||||
usageByProvider: enabled ? usageByProvider : EMPTY_USAGE_BY_PROVIDER,
|
||||
isLoading: enabled ? isLoading : false,
|
||||
loadRecentRequests,
|
||||
refreshRecentRequests,
|
||||
};
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useInterval } from '@/hooks/useInterval';
|
||||
import { USAGE_STATS_STALE_TIME_MS, useUsageStatsStore } from '@/stores';
|
||||
import type { KeyStats, UsageDetail } from '@/utils/usage';
|
||||
|
||||
const EMPTY_KEY_STATS: KeyStats = { bySource: {}, byAuthIndex: {} };
|
||||
const EMPTY_USAGE_DETAILS: UsageDetail[] = [];
|
||||
|
||||
export type UseProviderStatsOptions = {
|
||||
enabled?: boolean;
|
||||
};
|
||||
|
||||
export const useProviderStats = (options: UseProviderStatsOptions = {}) => {
|
||||
const enabled = options.enabled ?? true;
|
||||
const keyStats = useUsageStatsStore((state) => (enabled ? state.keyStats : EMPTY_KEY_STATS));
|
||||
const usageDetails = useUsageStatsStore((state) =>
|
||||
enabled ? state.usageDetails : EMPTY_USAGE_DETAILS
|
||||
);
|
||||
const isLoading = useUsageStatsStore((state) => (enabled ? state.loading : false));
|
||||
const loadUsageStats = useUsageStatsStore((state) => state.loadUsageStats);
|
||||
|
||||
// 首次进入页面优先复用缓存,避免跨页面重复拉取 /usage。
|
||||
const loadKeyStats = useCallback(async () => {
|
||||
await loadUsageStats({ staleTimeMs: USAGE_STATS_STALE_TIME_MS });
|
||||
}, [loadUsageStats]);
|
||||
|
||||
// 定时器触发时强制刷新共享 usage。
|
||||
const refreshKeyStats = useCallback(async () => {
|
||||
await loadUsageStats({ force: true, staleTimeMs: USAGE_STATS_STALE_TIME_MS });
|
||||
}, [loadUsageStats]);
|
||||
|
||||
useInterval(() => {
|
||||
void refreshKeyStats().catch(() => {});
|
||||
}, enabled ? 240_000 : null);
|
||||
|
||||
return { keyStats, usageDetails, loadKeyStats, refreshKeyStats, isLoading };
|
||||
};
|
||||
@@ -7,6 +7,6 @@ export { VertexSection } from './VertexSection';
|
||||
export { ProviderList } from './ProviderList';
|
||||
export { ProviderStatusBar } from './ProviderStatusBar';
|
||||
export { ProviderNav } from './ProviderNav';
|
||||
export * from './hooks/useProviderStats';
|
||||
export * from './hooks/useProviderRecentRequests';
|
||||
export * from './types';
|
||||
export * from './utils';
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { ApiKeyEntry, GeminiKeyConfig, ProviderKeyConfig } from '@/types';
|
||||
import type { HeaderEntry } from '@/utils/headers';
|
||||
import type { KeyStats, UsageDetail } from '@/utils/usage';
|
||||
|
||||
export interface ModelEntry {
|
||||
name: string;
|
||||
@@ -48,14 +47,3 @@ export type VertexFormState = Omit<ProviderKeyConfig, 'headers'> & {
|
||||
modelEntries: ModelEntry[];
|
||||
excludedText: string;
|
||||
};
|
||||
|
||||
export interface ProviderSectionProps<TConfig> {
|
||||
configs: TConfig[];
|
||||
keyStats: KeyStats;
|
||||
usageDetails: UsageDetail[];
|
||||
disabled: boolean;
|
||||
onEdit: (index: number) => void;
|
||||
onAdd: () => void;
|
||||
onDelete: (index: number) => void;
|
||||
onToggle?: (index: number, enabled: boolean) => void;
|
||||
}
|
||||
|
||||
@@ -6,18 +6,14 @@ import type {
|
||||
OpenAIProviderConfig,
|
||||
} from '@/types';
|
||||
import {
|
||||
buildCandidateUsageSourceIds,
|
||||
normalizeAuthIndex,
|
||||
type KeyStatBucket,
|
||||
type KeyStats,
|
||||
type UsageDetail,
|
||||
} from '@/utils/usage';
|
||||
import {
|
||||
collectUsageDetailsForAuthIndices,
|
||||
collectUsageDetailsForCandidates,
|
||||
type UsageDetailsByAuthIndex,
|
||||
type UsageDetailsBySource,
|
||||
} from '@/utils/usageIndex';
|
||||
buildRecentRequestCompositeKey,
|
||||
mergeRecentRequestBucketGroups,
|
||||
normalizeRecentRequestAuthIndex,
|
||||
statusBarDataFromRecentRequests,
|
||||
sumRecentRequests,
|
||||
type RecentRequestBucket,
|
||||
type StatusBarData,
|
||||
} from '@/utils/recentRequests';
|
||||
import type { AmpcodeFormState, AmpcodeUpstreamApiKeyEntry, ModelEntry } from './types';
|
||||
|
||||
export const DISABLE_ALL_MODELS_RULE = '*';
|
||||
@@ -103,161 +99,76 @@ export const buildClaudeMessagesEndpoint = (baseUrl: string): string => {
|
||||
return `${trimmed}/v1/messages`;
|
||||
};
|
||||
|
||||
// 根据 source (apiKey) 获取统计数据 - 与旧版逻辑一致
|
||||
export const getStatsBySource = (
|
||||
apiKey: string,
|
||||
keyStats: KeyStats,
|
||||
prefix?: string
|
||||
): KeyStatBucket => {
|
||||
const bySource = keyStats.bySource ?? {};
|
||||
const candidates = buildCandidateUsageSourceIds({ apiKey, prefix });
|
||||
if (!candidates.length) {
|
||||
return { success: 0, failure: 0 };
|
||||
}
|
||||
export type ProviderRecentUsageMap = Map<string, Map<string, RecentRequestBucket[]>>;
|
||||
|
||||
let success = 0;
|
||||
let failure = 0;
|
||||
candidates.forEach((candidate) => {
|
||||
const stats = bySource[candidate];
|
||||
if (!stats) return;
|
||||
success += stats.success;
|
||||
failure += stats.failure;
|
||||
});
|
||||
const normalizeProviderRecentKey = (value: unknown): string =>
|
||||
String(value ?? '').trim().toLowerCase();
|
||||
|
||||
return { success, failure };
|
||||
};
|
||||
|
||||
type UsageIdentity = {
|
||||
authIndex?: unknown;
|
||||
apiKey?: string;
|
||||
prefix?: string;
|
||||
};
|
||||
|
||||
export const getStatsForIdentity = (
|
||||
identity: UsageIdentity,
|
||||
keyStats: KeyStats
|
||||
): KeyStatBucket => {
|
||||
const authIndexKey = normalizeAuthIndex(identity.authIndex);
|
||||
if (authIndexKey) {
|
||||
const stats = keyStats.byAuthIndex?.[authIndexKey];
|
||||
if (stats) {
|
||||
return { success: stats.success, failure: stats.failure };
|
||||
}
|
||||
}
|
||||
|
||||
return getStatsBySource(identity.apiKey ?? '', keyStats, identity.prefix);
|
||||
};
|
||||
|
||||
export const collectUsageDetailsForIdentity = (
|
||||
identity: UsageIdentity,
|
||||
usageDetailsBySource: UsageDetailsBySource,
|
||||
usageDetailsByAuthIndex: UsageDetailsByAuthIndex
|
||||
): UsageDetail[] => {
|
||||
const authIndexKey = normalizeAuthIndex(identity.authIndex);
|
||||
if (authIndexKey) {
|
||||
const details = collectUsageDetailsForAuthIndices(usageDetailsByAuthIndex, [authIndexKey]);
|
||||
if (details.length > 0) {
|
||||
return details;
|
||||
}
|
||||
}
|
||||
|
||||
const candidates = buildCandidateUsageSourceIds({
|
||||
apiKey: identity.apiKey,
|
||||
prefix: identity.prefix,
|
||||
});
|
||||
if (!candidates.length) {
|
||||
export function getProviderRecentBuckets(
|
||||
usageByProvider: ProviderRecentUsageMap,
|
||||
provider: string,
|
||||
apiKey?: string,
|
||||
baseUrl?: string
|
||||
): RecentRequestBucket[] {
|
||||
if (!String(apiKey ?? '').trim()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return collectUsageDetailsForCandidates(usageDetailsBySource, candidates);
|
||||
};
|
||||
const providerKey = normalizeProviderRecentKey(provider);
|
||||
const compositeKey = buildRecentRequestCompositeKey(baseUrl, apiKey);
|
||||
return usageByProvider.get(providerKey)?.get(compositeKey) ?? [];
|
||||
}
|
||||
|
||||
const mergeUsageDetails = (groups: UsageDetail[][]): UsageDetail[] => {
|
||||
let firstDetails: UsageDetail[] | null = null;
|
||||
let merged: UsageDetail[] | null = null;
|
||||
export function getProviderRecentStats(
|
||||
usageByProvider: ProviderRecentUsageMap,
|
||||
provider: string,
|
||||
apiKey?: string,
|
||||
baseUrl?: string
|
||||
): { success: number; failure: number } {
|
||||
return sumRecentRequests(getProviderRecentBuckets(usageByProvider, provider, apiKey, baseUrl));
|
||||
}
|
||||
|
||||
groups.forEach((details) => {
|
||||
if (!details.length) return;
|
||||
if (!firstDetails) {
|
||||
firstDetails = details;
|
||||
return;
|
||||
}
|
||||
if (!merged) {
|
||||
merged = [...firstDetails];
|
||||
}
|
||||
merged.push(...details);
|
||||
});
|
||||
export function getProviderRecentStatusData(
|
||||
usageByProvider: ProviderRecentUsageMap,
|
||||
provider: string,
|
||||
apiKey?: string,
|
||||
baseUrl?: string
|
||||
): StatusBarData {
|
||||
return statusBarDataFromRecentRequests(
|
||||
getProviderRecentBuckets(usageByProvider, provider, apiKey, baseUrl)
|
||||
);
|
||||
}
|
||||
|
||||
return merged ?? firstDetails ?? [];
|
||||
};
|
||||
|
||||
// 对于 OpenAI 提供商,汇总所有 apiKeyEntries 的统计 - 与旧版逻辑一致
|
||||
export const getOpenAIProviderStats = (
|
||||
export function collectOpenAIProviderRecentBuckets(
|
||||
provider: OpenAIProviderConfig,
|
||||
keyStats: KeyStats
|
||||
): KeyStatBucket => {
|
||||
let success = 0;
|
||||
let failure = 0;
|
||||
|
||||
usageByProvider: ProviderRecentUsageMap
|
||||
): RecentRequestBucket[] {
|
||||
if (!provider.apiKeyEntries?.length) {
|
||||
const stats = getStatsForIdentity(
|
||||
{ authIndex: provider.authIndex, prefix: provider.prefix },
|
||||
keyStats
|
||||
);
|
||||
return { success: stats.success, failure: stats.failure };
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!normalizeAuthIndex(provider.authIndex) && provider.prefix) {
|
||||
const prefixStats = getStatsBySource('', keyStats, provider.prefix);
|
||||
success += prefixStats.success;
|
||||
failure += prefixStats.failure;
|
||||
}
|
||||
const groups = provider.apiKeyEntries.map((entry) =>
|
||||
getProviderRecentBuckets(usageByProvider, provider.name, entry.apiKey, provider.baseUrl)
|
||||
);
|
||||
|
||||
provider.apiKeyEntries.forEach((entry) => {
|
||||
const stats = getStatsForIdentity({ authIndex: entry.authIndex, apiKey: entry.apiKey }, keyStats);
|
||||
success += stats.success;
|
||||
failure += stats.failure;
|
||||
});
|
||||
return mergeRecentRequestBucketGroups(groups);
|
||||
}
|
||||
|
||||
return { success, failure };
|
||||
};
|
||||
|
||||
export const collectOpenAIProviderUsageDetails = (
|
||||
export function getOpenAIProviderRecentStats(
|
||||
provider: OpenAIProviderConfig,
|
||||
usageDetailsBySource: UsageDetailsBySource,
|
||||
usageDetailsByAuthIndex: UsageDetailsByAuthIndex
|
||||
): UsageDetail[] => {
|
||||
if (!provider.apiKeyEntries?.length) {
|
||||
return collectUsageDetailsForIdentity(
|
||||
{ authIndex: provider.authIndex, prefix: provider.prefix },
|
||||
usageDetailsBySource,
|
||||
usageDetailsByAuthIndex
|
||||
);
|
||||
}
|
||||
usageByProvider: ProviderRecentUsageMap
|
||||
): { success: number; failure: number } {
|
||||
return sumRecentRequests(collectOpenAIProviderRecentBuckets(provider, usageByProvider));
|
||||
}
|
||||
|
||||
const groups: UsageDetail[][] = [];
|
||||
if (!normalizeAuthIndex(provider.authIndex) && provider.prefix) {
|
||||
groups.push(
|
||||
collectUsageDetailsForIdentity(
|
||||
{ prefix: provider.prefix },
|
||||
usageDetailsBySource,
|
||||
usageDetailsByAuthIndex
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
provider.apiKeyEntries.forEach((entry) => {
|
||||
groups.push(
|
||||
collectUsageDetailsForIdentity(
|
||||
{ authIndex: entry.authIndex, apiKey: entry.apiKey },
|
||||
usageDetailsBySource,
|
||||
usageDetailsByAuthIndex
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
return mergeUsageDetails(groups);
|
||||
};
|
||||
export function getOpenAIProviderRecentStatusData(
|
||||
provider: OpenAIProviderConfig,
|
||||
usageByProvider: ProviderRecentUsageMap
|
||||
): StatusBarData {
|
||||
return statusBarDataFromRecentRequests(
|
||||
collectOpenAIProviderRecentBuckets(provider, usageByProvider)
|
||||
);
|
||||
}
|
||||
|
||||
export const getProviderConfigKey = (
|
||||
config: {
|
||||
@@ -268,7 +179,7 @@ export const getProviderConfigKey = (
|
||||
},
|
||||
index: number
|
||||
): string => {
|
||||
const authIndexKey = normalizeAuthIndex(config.authIndex);
|
||||
const authIndexKey = normalizeRecentRequestAuthIndex(config.authIndex);
|
||||
if (authIndexKey) {
|
||||
return authIndexKey;
|
||||
}
|
||||
@@ -276,7 +187,7 @@ export const getProviderConfigKey = (
|
||||
};
|
||||
|
||||
export const getOpenAIProviderKey = (provider: OpenAIProviderConfig, index: number): string => {
|
||||
const authIndexKey = normalizeAuthIndex(provider.authIndex);
|
||||
const authIndexKey = normalizeRecentRequestAuthIndex(provider.authIndex);
|
||||
if (authIndexKey) {
|
||||
return authIndexKey;
|
||||
}
|
||||
@@ -284,7 +195,7 @@ export const getOpenAIProviderKey = (provider: OpenAIProviderConfig, index: numb
|
||||
};
|
||||
|
||||
export const getOpenAIEntryKey = (entry: ApiKeyEntry, index: number): string => {
|
||||
const authIndexKey = normalizeAuthIndex(entry.authIndex);
|
||||
const authIndexKey = normalizeRecentRequestAuthIndex(entry.authIndex);
|
||||
if (authIndexKey) {
|
||||
return authIndexKey;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user