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:
Supra4E8C
2026-05-02 02:53:15 +08:00
parent 8ed837c3d7
commit b25f722fef
23 changed files with 505 additions and 473 deletions
@@ -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 };
};
+1 -1
View File
@@ -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';
-12
View File
@@ -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;
}
+65 -154
View File
@@ -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;
}