diff --git a/src/components/providers/utils.ts b/src/components/providers/utils.ts
index b2d2c82..3e92cfe 100644
--- a/src/components/providers/utils.ts
+++ b/src/components/providers/utils.ts
@@ -118,6 +118,16 @@ export function getProviderRecentStatusData(
);
}
+export function getProviderTotalStats(
+ usageByProvider: ProviderRecentUsageMap,
+ provider: string,
+ apiKey?: string,
+ baseUrl?: string
+): { success: number; failure: number } {
+ const entry = getProviderRecentUsageEntry(usageByProvider, provider, apiKey, baseUrl);
+ return { success: entry.success, failure: entry.failed };
+}
+
const collectOpenAIProviderRecentBuckets = (
provider: OpenAIProviderConfig,
usageByProvider: ProviderRecentUsageMap
@@ -140,6 +150,27 @@ export function getOpenAIProviderRecentWindowStats(
return sumRecentRequests(collectOpenAIProviderRecentBuckets(provider, usageByProvider));
}
+export function getOpenAIProviderTotalStats(
+ provider: OpenAIProviderConfig,
+ usageByProvider: ProviderRecentUsageMap
+): { success: number; failure: number } {
+ return (provider.apiKeyEntries || []).reduce(
+ (total, entry) => {
+ const usageEntry = getProviderRecentUsageEntry(
+ usageByProvider,
+ provider.name,
+ entry.apiKey,
+ provider.baseUrl
+ );
+ return {
+ success: total.success + usageEntry.success,
+ failure: total.failure + usageEntry.failed,
+ };
+ },
+ { success: 0, failure: 0 }
+ );
+}
+
export function getOpenAIProviderRecentStatusData(
provider: OpenAIProviderConfig,
usageByProvider: ProviderRecentUsageMap
diff --git a/src/features/providers/ProvidersWorkbenchPage.tsx b/src/features/providers/ProvidersWorkbenchPage.tsx
index 28feb8a..399177c 100644
--- a/src/features/providers/ProvidersWorkbenchPage.tsx
+++ b/src/features/providers/ProvidersWorkbenchPage.tsx
@@ -417,6 +417,7 @@ export function ProvidersWorkbenchPage() {
workbench={workbench}
onCreated={handleCreated}
onUpdated={handleUpdated}
+ usageByProvider={usageByProvider}
/>
);
diff --git a/src/features/providers/components/ProviderResourceTable.module.scss b/src/features/providers/components/ProviderResourceTable.module.scss
index 8315271..03c159b 100644
--- a/src/features/providers/components/ProviderResourceTable.module.scss
+++ b/src/features/providers/components/ProviderResourceTable.module.scss
@@ -112,6 +112,38 @@
min-width: 0;
}
+.stats {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+}
+
+.statPill {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ padding: 2px 8px;
+ border-radius: 999px;
+ font-size: 11px;
+ font-weight: 600;
+ line-height: 1.4;
+ border: 1px solid transparent;
+ white-space: nowrap;
+ font-variant-numeric: tabular-nums;
+}
+
+.statSuccess {
+ background-color: var(--success-badge-bg);
+ color: var(--success-badge-text);
+ border-color: var(--success-badge-border);
+}
+
+.statFailure {
+ background-color: var(--failure-badge-bg);
+ color: var(--failure-badge-text);
+ border-color: var(--failure-badge-border);
+}
+
.statusActive {
border-color: var(--success-badge-border);
background: var(--success-badge-bg);
diff --git a/src/features/providers/components/ProviderResourceTable.tsx b/src/features/providers/components/ProviderResourceTable.tsx
index 1ef2ca4..732a112 100644
--- a/src/features/providers/components/ProviderResourceTable.tsx
+++ b/src/features/providers/components/ProviderResourceTable.tsx
@@ -19,7 +19,9 @@ import { ToggleSwitch } from '@/components/ui/ToggleSwitch';
import { ProviderStatusBar } from '@/components/providers/ProviderStatusBar';
import {
getOpenAIProviderRecentStatusData,
+ getOpenAIProviderTotalStats,
getProviderRecentStatusData,
+ getProviderTotalStats,
type ProviderRecentUsageMap,
} from '@/components/providers/utils';
import type { OpenAIProviderConfig } from '@/types';
@@ -59,6 +61,24 @@ const resolveStatusBarData = (
);
};
+const resolveTotalStats = (
+ resource: ProviderResource,
+ usageByProvider: ProviderRecentUsageMap
+): { success: number; failure: number } => {
+ if (resource.brand === 'openaiCompatibility') {
+ return getOpenAIProviderTotalStats(
+ resource.raw as OpenAIProviderConfig,
+ usageByProvider
+ );
+ }
+ return getProviderTotalStats(
+ usageByProvider,
+ resource.brand,
+ resource.apiKey ?? undefined,
+ resource.baseUrl ?? undefined
+ );
+};
+
export function ProviderResourceTable({
resources,
selectedId,
@@ -224,10 +244,25 @@ export function ProviderResourceTable({
{renderStatus(resource)}
{usageByProvider && resource.brand !== 'ampcode' ? (
-
+ <>
+ {(() => {
+ const stats = resolveTotalStats(resource, usageByProvider);
+ return (
+
+
+ {t('stats.success')}: {stats.success}
+
+
+ {t('stats.failure')}: {stats.failure}
+
+
+ );
+ })()}
+
+ >
) : null}
diff --git a/src/features/providers/sheets/ProviderSheet.tsx b/src/features/providers/sheets/ProviderSheet.tsx
index 50c6427..9c4f904 100644
--- a/src/features/providers/sheets/ProviderSheet.tsx
+++ b/src/features/providers/sheets/ProviderSheet.tsx
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useId, useImperativeHandle, useState, type Ref
import { useTranslation } from 'react-i18next';
import { Sheet } from '@/components/ui/Sheet';
import { IconLoader2, IconPencil } from '@/components/ui/icons';
+import type { ProviderRecentUsageMap } from '@/components/providers/utils';
import { useNotificationStore } from '@/stores';
import { PROVIDER_DESCRIPTORS } from '../descriptors';
import type {
@@ -35,6 +36,7 @@ interface ProviderSheetProps {
workbench: UseProviderWorkbenchResult;
onCreated: () => void;
onUpdated: () => void;
+ usageByProvider?: ProviderRecentUsageMap;
ref?: Ref;
}
@@ -45,6 +47,7 @@ export function ProviderSheet({
workbench,
onCreated,
onUpdated,
+ usageByProvider,
ref,
}: ProviderSheetProps) {
const { t } = useTranslation();
@@ -151,7 +154,7 @@ export function ProviderSheet({
if (!state.resource) {
return null;
}
- return ;
+ return ;
}
const formKey = `${state.brand}:${state.resource?.id ?? 'new'}:${state.mode}`;
if (isAmpcode) {
diff --git a/src/features/providers/sheets/ResourceDetailView.tsx b/src/features/providers/sheets/ResourceDetailView.tsx
index e331b89..f22f41f 100644
--- a/src/features/providers/sheets/ResourceDetailView.tsx
+++ b/src/features/providers/sheets/ResourceDetailView.tsx
@@ -1,13 +1,21 @@
import { useTranslation } from 'react-i18next';
import { Collapsible } from '@/components/ui/Collapsible';
+import { IconCheck, IconX } from '@/components/ui/icons';
+import {
+ getProviderTotalStats,
+ type ProviderRecentUsageMap,
+} from '@/components/providers/utils';
+import type { OpenAIProviderConfig } from '@/types';
+import { maskApiKey } from '@/utils/format';
import type { ProviderResource } from '../types';
import styles from './forms/sharedForm.module.scss';
interface ResourceDetailViewProps {
resource: ProviderResource;
+ usageByProvider?: ProviderRecentUsageMap;
}
-export function ResourceDetailView({ resource }: ResourceDetailViewProps) {
+export function ResourceDetailView({ resource, usageByProvider }: ResourceDetailViewProps) {
const { t } = useTranslation();
const primary: Array<[string, string]> = [
@@ -25,6 +33,12 @@ export function ResourceDetailView({ resource }: ResourceDetailViewProps) {
['apiKeyEntries', String(resource.apiKeyEntryCount)],
];
+ const openaiConfig =
+ resource.brand === 'openaiCompatibility'
+ ? (resource.raw as OpenAIProviderConfig)
+ : null;
+ const apiKeyEntries = openaiConfig?.apiKeyEntries ?? [];
+
return (
@@ -42,6 +56,50 @@ export function ResourceDetailView({ resource }: ResourceDetailViewProps) {
))}
+ {openaiConfig && apiKeyEntries.length > 0 ? (
+
+
+ {t('providersPage.form.apiKeyEntriesSection')}: {apiKeyEntries.length}
+
+
+ {apiKeyEntries.map((entry, entryIndex) => {
+ const entryStats = usageByProvider
+ ? getProviderTotalStats(
+ usageByProvider,
+ openaiConfig.name,
+ entry.apiKey,
+ openaiConfig.baseUrl
+ )
+ : { success: 0, failure: 0 };
+ return (
+
+
{entryIndex + 1}
+
{maskApiKey(entry.apiKey)}
+ {entry.proxyUrl ? (
+
{entry.proxyUrl}
+ ) : null}
+
+
+ {entryStats.success}
+
+
+ {entryStats.failure}
+
+
+
+ );
+ })}
+
+
+ ) : null}
+
diff --git a/src/features/providers/sheets/forms/sharedForm.module.scss b/src/features/providers/sheets/forms/sharedForm.module.scss
index 1e5773f..9a1ecf8 100644
--- a/src/features/providers/sheets/forms/sharedForm.module.scss
+++ b/src/features/providers/sheets/forms/sharedForm.module.scss
@@ -585,3 +585,106 @@
background: var(--primary-hover);
}
}
+
+.apiKeyEntriesSection {
+ margin-top: 16px;
+ min-width: 0;
+ max-width: 100%;
+}
+
+.apiKeyEntriesLabel {
+ font-size: 12px;
+ font-weight: 600;
+ color: var(--text-secondary);
+ margin-bottom: 6px;
+}
+
+.apiKeyEntryList {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ min-width: 0;
+ max-width: 100%;
+}
+
+.apiKeyEntryCard {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 8px;
+ min-width: 0;
+ max-width: 100%;
+ padding: 8px 12px;
+ background: var(--bg-secondary);
+ border: 1px solid var(--border-secondary);
+ border-radius: 8px;
+ font-size: 12px;
+}
+
+.apiKeyEntryIndex {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 20px;
+ height: 20px;
+ border-radius: 50%;
+ background: var(--primary-color);
+ color: var(--primary-contrast);
+ font-size: 11px;
+ font-weight: 600;
+ flex-shrink: 0;
+}
+
+.apiKeyEntryKey {
+ font-family: 'Monaco', 'Menlo', 'Consolas', 'Ubuntu Mono', monospace;
+ font-weight: 600;
+ color: var(--text-primary);
+ min-width: 0;
+ max-width: 100%;
+ overflow-wrap: anywhere;
+ word-break: break-all;
+}
+
+.apiKeyEntryProxy {
+ color: var(--text-tertiary);
+ font-size: 11px;
+ min-width: 0;
+ max-width: 100%;
+ overflow-wrap: anywhere;
+ word-break: break-word;
+
+ &::before {
+ content: '| Proxy: ';
+ color: var(--text-quaternary);
+ }
+}
+
+.apiKeyEntryStats {
+ display: flex;
+ gap: 6px;
+ margin-left: auto;
+}
+
+.apiKeyEntryStat {
+ display: inline-flex;
+ align-items: center;
+ gap: 3px;
+ padding: 2px 6px;
+ border-radius: 10px;
+ font-size: 10px;
+ font-weight: 600;
+
+ svg {
+ display: block;
+ }
+}
+
+.apiKeyEntryStatSuccess {
+ background: var(--success-badge-bg);
+ color: var(--success-badge-text);
+}
+
+.apiKeyEntryStatFailure {
+ background: var(--failure-badge-bg);
+ color: var(--failure-badge-text);
+}