Skip to content

feat(credentials): add relay provider usage panel for AI providers - #321

Open
Guoyin-Wen wants to merge 3 commits into
Willxup:mainfrom
Guoyin-Wen:feat/relay-provider-usage
Open

feat(credentials): add relay provider usage panel for AI providers#321
Guoyin-Wen wants to merge 3 commits into
Willxup:mainfrom
Guoyin-Wen:feat/relay-provider-usage

Conversation

@Guoyin-Wen

@Guoyin-Wen Guoyin-Wen commented Jul 17, 2026

Copy link
Copy Markdown

What

Adds a relay provider usage panel that queries upstream relay providers (GLM, MiniMax, Kimi, DeepSeek) for balance and quota-window usage, shown as an optional Usage column on the AI Provider credentials page.

3840X1822/image.png

Why

AI Provider credentials currently surface only request activity. For providers that expose a usage/balance endpoint, showing live quota-window usage (e.g. 5h tokens, weekly tokens, monthly MCP) alongside health helps operators spot accounts near their limits without leaving the dashboard.

Changes

Backend

  • internal/relayusage: per-provider adapters (GLM, MiniMax, Kimi, DeepSeek) behind a shared service + matcher + normalize layer
  • internal/api/relay_usage.go: endpoint wired into the router

Frontend

  • AiProviderUsagePanel + useRelayProviderUsage hook
  • CredentialRowShell / CredentialTableHeader gain an optional usage column (off by default; only rendered when the host passes onSetRelayPlatform)
  • i18n keys for en / zh-CN / zh-TW
  • formatQuotaResetDuration moved to CredentialSectionShell and localized

Testing

  • go test ./cmd/... ./internal/... — pass (incl. new internal/relayusage adapter/matcher tests)
  • npm run typecheck / lint / build — pass
  • npm run test — pass (credential style + quota-reset tests updated to match the new row structure)

Notes

The "Usage" column is opt-in: it only renders when onSetRelayPlatform is provided, so sections that don't opt in keep the original three-column layout.

Built on top of the latest main (includes the credential health visualization and speed-mode changes).

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new feature to query and display relay provider usage (GLM, MiniMax, Kimi, and DeepSeek) directly, isolating it from the OAuth quota path. It adds backend adapters, matchers, and a service along with frontend UI panels, state management, and translations. The review feedback is highly constructive and should be addressed: it suggests preventing goroutine blocking on context cancellation in the Go service, avoiding disabling React hooks lint rules, caching Intl.NumberFormat instances to optimize performance, displaying raw unclamped percentage values in the UI, and improving accessibility by closing the platform switcher on Escape key press.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +80 to +85
go func(i int, identityID string) {
defer wg.Done()
s.workerSem <- struct{}{}
defer func() { <-s.workerSem }()
items[i] = s.fetchOne(ctx, identityID, overrides)
}(idx, id)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When the context is cancelled, goroutines waiting to acquire the worker semaphore will block unnecessarily until they can acquire it, even though the context is already cancelled. Using a select statement to race the semaphore acquisition against ctx.Done() prevents this blocking and allows immediate cleanup.

		go func(i int, identityID string) {
			defer wg.Done()
			select {
			case s.workerSem <- struct{}{}:
				defer func() { <-s.workerSem }()
				items[i] = s.fetchOne(ctx, identityID, overrides)
			case <-ctx.Done():
				items[i] = UsageItem{IdentityID: identityID, Skipped: ctx.Err().Error()}
			}
		}(idx, id)

Comment on lines +101 to +105
useEffect(() => {
void loadAssignments(identityIds)
void refreshUsage(identityIds)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [idsKey, loadAssignments, refreshUsage])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Instead of disabling the react-hooks/exhaustive-deps lint rule, you can parse the idsKey inside the useEffect hook. This keeps the dependency array clean and compliant with standard React linting rules.

Suggested change
useEffect(() => {
void loadAssignments(identityIds)
void refreshUsage(identityIds)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [idsKey, loadAssignments, refreshUsage])
useEffect(() => {
const ids = idsKey ? idsKey.split(',') : []
void loadAssignments(ids)
void refreshUsage(ids)
}, [idsKey, loadAssignments, refreshUsage])

Comment on lines +197 to +204
function formatRelayAmount(value: number, currency: string): string {
const amount = Number.isFinite(value) ? value : 0
try {
return new Intl.NumberFormat('zh-CN', { style: 'currency', currency, minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(amount)
} catch {
return `${amount.toFixed(2)} ${currency}`
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Constructing a new Intl.NumberFormat instance on every call to formatRelayAmount is highly inefficient and can cause significant garbage collection pressure and CPU overhead under heavy load. Caching the formatters by currency avoids this overhead.

Suggested change
function formatRelayAmount(value: number, currency: string): string {
const amount = Number.isFinite(value) ? value : 0
try {
return new Intl.NumberFormat('zh-CN', { style: 'currency', currency, minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(amount)
} catch {
return `${amount.toFixed(2)} ${currency}`
}
}
const relayAmountFormatters = new Map<string, Intl.NumberFormat>()
function getRelayAmountFormatter(currency: string): Intl.NumberFormat {
let formatter = relayAmountFormatters.get(currency)
if (!formatter) {
formatter = new Intl.NumberFormat('zh-CN', { style: 'currency', currency, minimumFractionDigits: 2, maximumFractionDigits: 2 })
relayAmountFormatters.set(currency, formatter)
}
return formatter
}
function formatRelayAmount(value: number, currency: string): string {
const amount = Number.isFinite(value) ? value : 0
try {
return getRelayAmountFormatter(currency).format(amount)
} catch {
return amount.toFixed(2) + " " + currency
}
}

Comment on lines +119 to +121
const percent = quota.barPercent ?? 0
const width = `${Math.max(0, Math.min(100, percent))}%`
const percentLabel = quota.barPercent === null ? '—' : `${Math.round(quota.barPercent)}%`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To ensure upstream data anomalies remain visible, calculated metrics (such as percentages) should display their raw, unclamped values, while only the visual progress bar width should be clamped to prevent layout breakage.

Suggested change
const percent = quota.barPercent ?? 0
const width = `${Math.max(0, Math.min(100, percent))}%`
const percentLabel = quota.barPercent === null ? '—' : `${Math.round(quota.barPercent)}%`
const percent = quota.barPercent ?? 0
const width = Math.max(0, Math.min(100, percent)) + "%"
const rawPercent = quota.percent !== undefined && quota.percent !== null
? (quota.percentKind === 'used' ? quota.percent : 100 - quota.percent)
: null
const percentLabel = rawPercent === null ? '—' : Math.round(rawPercent) + "%"

Comment on lines +69 to +76
useEffect(() => {
if (!open) return
const handler = (event: MouseEvent) => {
if (ref.current && !ref.current.contains(event.target as Node)) setOpen(false)
}
document.addEventListener('mousedown', handler)
return () => document.removeEventListener('mousedown', handler)
}, [open])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For better usability and accessibility, the platform switcher dropdown menu should also close when the user presses the Escape key.

  useEffect(() => {
    if (!open) return
    const handleOutsideClick = (event: MouseEvent) => {
      if (ref.current && !ref.current.contains(event.target as Node)) setOpen(false)
    }
    const handleKeyDown = (event: KeyboardEvent) => {
      if (event.key === 'Escape') setOpen(false)
    }
    document.addEventListener('mousedown', handleOutsideClick)
    document.addEventListener('keydown', handleKeyDown)
    return () => {
      document.removeEventListener('mousedown', handleOutsideClick)
      document.removeEventListener('keydown', handleKeyDown)
    }
  }, [open])

devin and others added 2 commits July 18, 2026 06:02
Query upstream relay providers (GLM, MiniMax, Kimi, DeepSeek) for balance
and quota-window usage, surfaced as an optional "Usage" column on the AI
Provider credentials page.

- internal/relayusage: per-provider adapters with shared matcher/normalize
- internal/api: relay usage endpoint wired into the router
- web: AiProviderUsagePanel + useRelayProviderUsage hook, i18n keys (en/zh/tw)
- CredentialRowShell/TableHeader gain an optional usage column
- move formatQuotaResetDuration to CredentialSectionShell and localize it

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Guoyin-Wen
Guoyin-Wen force-pushed the feat/relay-provider-usage branch from 2a26598 to c6621db Compare July 17, 2026 22:02
- internal/relayusage: listen to ctx.Done() when acquiring worker
- web: fix exhaustive-deps lint by moving id parsing into useEffect
- web: cache Intl.NumberFormat instances by currency
- web: only clamp UI width, display raw percent value
- web: add Escape key to close platform dropdown
@Guoyin-Wen
Guoyin-Wen force-pushed the feat/relay-provider-usage branch from 62c7d49 to 33bf410 Compare July 17, 2026 23:15
@Guoyin-Wen

Copy link
Copy Markdown
Author

Thanks Gemini for the suggestions! All 5 medium-priority changes have been applied:

  1. Go backend: Add select to check ctx.Done() when acquiring worker
  2. React hook: Remove eslint-disable and move id parsing inside useEffect
  3. Performance: Cache Intl.NumberFormat instances by currency
  4. Display: Only clamp UI width, keep raw percent visible
  5. Accessibility: Add Escape key to close platform dropdown

All tests passing.

@Willxup

Willxup commented Jul 21, 2026

Copy link
Copy Markdown
Owner

Thank you for the contribution. The core idea is valuable, but the current UI and interaction design do not align with the project’s established patterns. The Auth Files section already supports quota queries and uses a section-level switch to toggle between quota and health views. This PR introduces a separate, permanently visible Usage column, which is inconsistent with the rest of the credentials page.
I also do not have access to enough provider account types to validate all of these integrations, so I cannot confidently determine whether the feature works correctly across every supported provider. The usage responses currently vary significantly between providers, and the proposed backend structure would add substantial maintenance work for future iterations.
At the moment, I do not have the review bandwidth or validation environment required to safely accept a change of this size. I first need to complete the existing product work, performance optimizations, and ongoing refactoring.
For these reasons, I’m unable to accept this PR in its current form. Thank you again for the time and effort you put into it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants