- {/* Current Balance Display */}
-
+
{t('Current Balance')}
-
- {balance !== null
- ? formatBalance(balance)
- : formatBalance(currentRow.balance)}
-
+
{displayedAmount}
{t('Last updated:')}{' '}
- {formatDate(balanceUpdatedTime ?? currentRow.balance_updated_time)}
+ {displayedUpdatedAt > 0
+ ? formatTimestampToDate(displayedUpdatedAt)
+ : t('Never')}
diff --git a/web/src/features/channels/lib/__tests__/new-api-balance.test.ts b/web/src/features/channels/lib/__tests__/new-api-balance.test.ts
new file mode 100644
index 000000000000..b46aa35a5c31
--- /dev/null
+++ b/web/src/features/channels/lib/__tests__/new-api-balance.test.ts
@@ -0,0 +1,71 @@
+/*
+Copyright (C) 2023-2026 QuantumNous
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see
.
+
+For commercial licensing, please contact support@quantumnous.com
+*/
+import assert from 'node:assert/strict'
+import { describe, test } from 'node:test'
+
+import type { Channel, ChannelBalanceInfo } from '../../types'
+import { channelNeedsAttention } from '../channel-utils'
+import { formatNewAPIBalance } from '../new-api-balance'
+
+const balance = (
+ overrides: Partial
= {}
+): ChannelBalanceInfo => ({
+ remaining: '123.45',
+ unit: 'money',
+ currency: 'USD',
+ unlimited: false,
+ updated_at: 1_786_000_000,
+ ...overrides,
+})
+
+describe('New API balance', () => {
+ test('formats money, credits and unlimited quota', () => {
+ assert.equal(formatNewAPIBalance(balance(), 'Unlimited'), '$123.45')
+ assert.equal(
+ formatNewAPIBalance(
+ balance({
+ unit: 'credits',
+ currency: undefined,
+ display_unit: 'credits',
+ }),
+ 'Unlimited'
+ ),
+ '123.45 credits'
+ )
+ assert.equal(
+ formatNewAPIBalance(balance({ unlimited: true }), '无限制'),
+ '无限制'
+ )
+ })
+
+ test('does not apply the legacy USD warning to native balances', () => {
+ const channel = {
+ id: 9,
+ type: 60,
+ status: 1,
+ balance: 0.5,
+ balance_info: balance({ currency: 'CNY', remaining: '0.5' }),
+ } as Channel
+ assert.equal(channelNeedsAttention(channel), false)
+ assert.equal(
+ channelNeedsAttention({ ...channel, balance_info: null }),
+ true
+ )
+ })
+})
diff --git a/web/src/features/channels/lib/channel-actions.ts b/web/src/features/channels/lib/channel-actions.ts
index 7efee24d3da2..e9f2de2c2da3 100644
--- a/web/src/features/channels/lib/channel-actions.ts
+++ b/web/src/features/channels/lib/channel-actions.ts
@@ -42,6 +42,7 @@ import {
} from '../api'
import { CHANNEL_STATUS, ERROR_MESSAGES, SUCCESS_MESSAGES } from '../constants'
import type { ChannelTestResponse, CopyChannelParams } from '../types'
+import { formatNewAPIBalance } from './new-api-balance'
// ============================================================================
// Query Keys
@@ -372,19 +373,31 @@ export async function handleUpdateChannelBalance(
): Promise {
try {
const response = await updateChannelBalance(id)
- if (response.success && response.balance !== undefined) {
- const balance = response.balance
+ const hasPayload =
+ response.data !== undefined || response.balance !== undefined
+ if (response.success && hasPayload) {
+ queryClient?.invalidateQueries({ queryKey: channelsQueryKeys.lists() })
+ let displayBalance = '-'
+ if (response.data) {
+ displayBalance = formatNewAPIBalance(
+ response.data,
+ i18next.t('Unlimited')
+ )
+ } else if (response.balance !== undefined) {
+ displayBalance = formatCurrencyFromUSD(response.balance, {
+ digitsLarge: 2,
+ digitsSmall: 4,
+ abbreviate: false,
+ })
+ }
toast.success(
i18next.t('Balance updated: {{balance}}', {
- balance: formatCurrencyFromUSD(balance, {
- digitsLarge: 2,
- digitsSmall: 4,
- abbreviate: false,
- }),
+ balance: displayBalance,
})
)
- queryClient?.invalidateQueries({ queryKey: channelsQueryKeys.lists() })
- onSuccess?.(balance)
+ if (response.balance !== undefined) {
+ onSuccess?.(response.balance)
+ }
} else {
toast.error(response.message || i18next.t('Failed to update balance'))
}
@@ -462,7 +475,9 @@ export async function handleBatchEnable(
toast.error(response.message || i18next.t('Failed to enable channels'))
} else if (failCount > 0) {
toast.error(
- i18next.t('{{count}} channel(s) failed to enable', { count: failCount })
+ i18next.t('{{count}} channel(s) failed to enable', {
+ count: failCount,
+ })
)
}
} catch {
diff --git a/web/src/features/channels/lib/channel-utils.ts b/web/src/features/channels/lib/channel-utils.ts
index 9424a8521b6f..7daac7b0f7c0 100644
--- a/web/src/features/channels/lib/channel-utils.ts
+++ b/web/src/features/channels/lib/channel-utils.ts
@@ -21,6 +21,7 @@ import { formatTimestampToDate } from '@/lib/format'
import {
CHANNEL_STATUS_CONFIG,
+ CHANNEL_TYPE_NEW_API,
CHANNEL_TYPES,
MULTI_KEY_STATUS_CONFIG,
RESPONSE_TIME_CONFIG,
@@ -561,8 +562,11 @@ export function channelNeedsAttention(channel: Channel): boolean {
return true
}
- // Low balance (less than $1)
- if (channel.balance > 0 && channel.balance < 1) {
+ // Legacy balances are USD. Structured native balances must not be compared
+ // against the legacy one-dollar threshold.
+ const hasNewAPIBalance =
+ channel.type === CHANNEL_TYPE_NEW_API && channel.balance_info
+ if (!hasNewAPIBalance && channel.balance > 0 && channel.balance < 1) {
return true
}
@@ -586,7 +590,9 @@ export function getAttentionReason(channel: Channel): string | null {
if (channel.status === 3) {
return 'Auto-disabled'
}
- if (channel.balance > 0 && channel.balance < 1) {
+ const hasNewAPIBalance =
+ channel.type === CHANNEL_TYPE_NEW_API && channel.balance_info
+ if (!hasNewAPIBalance && channel.balance > 0 && channel.balance < 1) {
return 'Low balance'
}
if (
diff --git a/web/src/features/channels/lib/index.ts b/web/src/features/channels/lib/index.ts
index 8c18151ceb5f..aada1dcddc8b 100644
--- a/web/src/features/channels/lib/index.ts
+++ b/web/src/features/channels/lib/index.ts
@@ -18,6 +18,7 @@ For commercial licensing, please contact support@quantumnous.com
*/
// Re-export all library functions
export * from './channel-actions'
+export * from './new-api-balance'
export * from './channel-field-update'
export * from './advanced-custom'
export * from './channel-form-errors'
diff --git a/web/src/features/channels/lib/new-api-balance.ts b/web/src/features/channels/lib/new-api-balance.ts
new file mode 100644
index 000000000000..c8460e1046c7
--- /dev/null
+++ b/web/src/features/channels/lib/new-api-balance.ts
@@ -0,0 +1,43 @@
+/*
+Copyright (C) 2023-2026 QuantumNous
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see .
+
+For commercial licensing, please contact support@quantumnous.com
+*/
+import type { ChannelBalanceInfo } from '../types'
+
+const CURRENCY_SYMBOLS: Record = {
+ CNY: '¥',
+ EUR: '€',
+ GBP: '£',
+ JPY: '¥',
+ KRW: '₩',
+ USD: '$',
+}
+
+export function formatNewAPIBalance(
+ info: ChannelBalanceInfo,
+ unlimitedLabel: string
+): string {
+ if (info.unlimited) return unlimitedLabel
+ const amount = info.remaining?.trim()
+ if (!amount) return '-'
+ if (info.unit === 'money') {
+ const currency = info.currency?.toUpperCase() || ''
+ const symbol = info.display_unit?.trim() || CURRENCY_SYMBOLS[currency] || ''
+ return `${symbol}${amount}`.trim()
+ }
+ return info.display_unit ? `${amount} ${info.display_unit}` : amount
+}
diff --git a/web/src/features/channels/types.ts b/web/src/features/channels/types.ts
index f7747fa21210..df97f31702b0 100644
--- a/web/src/features/channels/types.ts
+++ b/web/src/features/channels/types.ts
@@ -34,6 +34,20 @@ export const channelInfoSchema = z.object({
export type ChannelInfo = z.infer
+export const channelBalanceUnitSchema = z.enum(['money', 'tokens', 'credits'])
+
+export const channelBalanceInfoSchema = z.object({
+ remaining: z.string().nullish(),
+ unit: channelBalanceUnitSchema.nullish(),
+ currency: z.string().nullish(),
+ display_unit: z.string().nullish(),
+ unlimited: z.boolean(),
+ updated_at: z.number(),
+})
+
+export type ChannelBalanceUnit = z.infer
+export type ChannelBalanceInfo = z.infer
+
export const channelSchema = z.object({
id: z.number(),
type: z.number(),
@@ -50,6 +64,7 @@ export const channelSchema = z.object({
other: z.string().default(''),
balance: z.number().default(0), // in USD
balance_updated_time: z.number(),
+ balance_info: channelBalanceInfoSchema.nullish(),
models: z.string().default(''),
group: z.string().default('default'),
used_quota: z.number().default(0),
@@ -197,6 +212,7 @@ export interface ChannelBalanceResponse {
message?: string
balance?: number
currency?: string
+ data?: ChannelBalanceInfo
}
export interface FetchModelsResponse {