diff --git a/backend/crates/gateway-admin/src/model/provider_credentials.rs b/backend/crates/gateway-admin/src/model/provider_credentials.rs index 0c25c44ed..a7fa1c039 100644 --- a/backend/crates/gateway-admin/src/model/provider_credentials.rs +++ b/backend/crates/gateway-admin/src/model/provider_credentials.rs @@ -826,11 +826,20 @@ pub struct ProviderQuota { pub observed_at: Option>, pub refresh_token_expires_at: Option>, pub windows: Vec, + pub credits: Option, /// 展示用快照级触顶事实(顶层或任一窗口触顶);不参与账号五态派生。 pub limit_reached: bool, pub provider_data: Option, } +/// 上游点数余额的展示事实,保留原始十进制文本以避免精度损失。 +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ProviderQuotaCredits { + pub has_credits: bool, + pub unlimited: bool, + pub balance: Option, +} + /// 空值和 `unknown` 代表未提供套餐;新的套餐标识仍按明确值保留。 pub(crate) fn explicit_plan_type(value: Option<&str>) -> Option<&str> { value.filter(|value| { diff --git a/backend/crates/gateway-admin/src/use_case/accounts.rs b/backend/crates/gateway-admin/src/use_case/accounts.rs index 5830222ee..0daafe833 100644 --- a/backend/crates/gateway-admin/src/use_case/accounts.rs +++ b/backend/crates/gateway-admin/src/use_case/accounts.rs @@ -1052,6 +1052,7 @@ fn map_reset_credits_error_after_refresh( /// 账号目录中单个账号 quota 读取失败时使用的空额度投影。 fn empty_quota() -> ProviderQuota { ProviderQuota { + credits: None, plan_type: None, observed_at: None, refresh_token_expires_at: None, diff --git a/backend/crates/gateway-admin/tests/main.rs b/backend/crates/gateway-admin/tests/main.rs index 7743852bc..448160f15 100644 --- a/backend/crates/gateway-admin/tests/main.rs +++ b/backend/crates/gateway-admin/tests/main.rs @@ -129,6 +129,7 @@ fn provider_document_debug_should_not_expose_opaque_material() { #[test] fn representative_quota_should_prefer_short_window_and_highest_usage() { let quota = ProviderQuota { + credits: None, plan_type: None, observed_at: None, refresh_token_expires_at: None, @@ -150,6 +151,7 @@ fn representative_quota_should_prefer_account_wide_window_over_model_specific_wi let mut model_specific = quota_window("shortTerm", Some(18_000), Some(64.0)); model_specific.local_usage_attribution = QuotaLocalUsageAttribution::Unavailable; let quota = ProviderQuota { + credits: None, plan_type: None, observed_at: None, refresh_token_expires_at: None, @@ -187,6 +189,7 @@ fn usage_window_should_not_use_daily_rolling_usage_for_weekly_statistics() { models: Vec::new(), }; let quota = ProviderQuota { + credits: None, plan_type: None, observed_at: None, refresh_token_expires_at: None, @@ -215,6 +218,7 @@ fn usage_window_should_not_use_daily_rolling_usage_for_weekly_statistics() { #[test] fn exhausted_quota_should_project_full_usage_to_only_the_representative_window() { let mut quota = ProviderQuota { + credits: None, plan_type: None, observed_at: None, refresh_token_expires_at: None, @@ -243,6 +247,7 @@ fn exhausted_quota_should_preserve_the_provider_identified_reached_window() { let mut reached = quota_window("monthly", Some(2_592_000), Some(98.0)); reached.limit_reached = true; let mut quota = ProviderQuota { + credits: None, plan_type: None, observed_at: None, refresh_token_expires_at: None, diff --git a/backend/crates/gateway-admin/tests/model/provider_credentials.rs b/backend/crates/gateway-admin/tests/model/provider_credentials.rs index 291fd9640..5b2cdac4a 100644 --- a/backend/crates/gateway-admin/tests/model/provider_credentials.rs +++ b/backend/crates/gateway-admin/tests/model/provider_credentials.rs @@ -84,6 +84,7 @@ fn selected_usage_window(quota: &ProviderQuota) -> Option<(&str, AccountUsagePer fn usage_quota(windows: Vec) -> ProviderQuota { ProviderQuota { + credits: None, plan_type: None, observed_at: None, refresh_token_expires_at: None, @@ -133,6 +134,7 @@ fn usage_window(key: &str, group: &str, seconds: u64) -> ProviderQuotaWindow { #[test] fn dashboard_quota_should_preserve_unknown_and_actual_window_facts() { let mut quota = ProviderQuota { + credits: None, plan_type: None, observed_at: None, refresh_token_expires_at: None, diff --git a/backend/crates/gateway-admin/tests/model/quota_forecast.rs b/backend/crates/gateway-admin/tests/model/quota_forecast.rs index d54fd20b2..945816f27 100644 --- a/backend/crates/gateway-admin/tests/model/quota_forecast.rs +++ b/backend/crates/gateway-admin/tests/model/quota_forecast.rs @@ -56,6 +56,7 @@ fn window(key: &str, days: u64) -> ProviderQuotaWindow { fn quota(windows: Vec) -> ProviderQuota { ProviderQuota { + credits: None, plan_type: None, observed_at: Some(now()), refresh_token_expires_at: None, diff --git a/backend/crates/gateway-admin/tests/use_case/accounts.rs b/backend/crates/gateway-admin/tests/use_case/accounts.rs index de65d53c2..c716fc2c0 100644 --- a/backend/crates/gateway-admin/tests/use_case/accounts.rs +++ b/backend/crates/gateway-admin/tests/use_case/accounts.rs @@ -1655,6 +1655,7 @@ async fn accounts_recover_disabled_should_only_enable_scheduling_and_preserve_qu ); } let quota = ProviderQuota { + credits: None, observed_at: Some(Utc::now()), windows: vec![ProviderQuotaWindow { key: "primary".to_owned(), @@ -1993,6 +1994,7 @@ async fn accounts_list_should_degrade_quota_failure_to_empty_window_without_drop let events = events(); let openai = FakeProviderAdmin::new("openai", events.clone()); openai.set_quota(ProviderQuota { + credits: None, plan_type: None, observed_at: Some(Utc::now()), refresh_token_expires_at: None, @@ -2125,6 +2127,7 @@ async fn accounts_list_should_map_unknown_credential_to_error_not_normal() { async fn accounts_list_should_not_derive_rate_limited_from_provider_quota_view() { let provider = FakeProviderAdmin::new("openai", events()); provider.set_quota(ProviderQuota { + credits: None, plan_type: None, observed_at: Some(Utc::now()), refresh_token_expires_at: None, @@ -2173,6 +2176,7 @@ async fn accounts_list_should_not_derive_rate_limited_from_provider_quota_view() async fn accounts_list_should_not_derive_exhaustion_from_provider_quota_view() { let provider = FakeProviderAdmin::new("openai", events()); provider.set_quota(ProviderQuota { + credits: None, plan_type: None, observed_at: Some(Utc::now()), refresh_token_expires_at: None, @@ -2224,6 +2228,7 @@ async fn quota_forecast_reads_raw_snapshot_and_limits_usage_to_observation_time( let observed = now - TimeDelta::hours(1); let reset = now + TimeDelta::days(1); provider.set_quota(ProviderQuota { + credits: None, observed_at: Some(observed), limit_reached: true, windows: vec![ProviderQuotaWindow { @@ -2378,6 +2383,7 @@ async fn quota_forecast_mid_cycle_sampling_accepts_small_reset_jitter_but_not_a_ let added = now - TimeDelta::hours(5); let provider = FakeProviderAdmin::new("openai", events()); provider.set_quota(ProviderQuota { + credits: None, plan_type: Some("pro".to_owned()), observed_at: Some(observed), windows: vec![ProviderQuotaWindow { @@ -2524,6 +2530,7 @@ async fn accounts_list_should_attach_local_usage_to_quota_windows() { let provider = FakeProviderAdmin::new("openai", events()); let reset_at = Utc::now() + TimeDelta::hours(1); provider.set_quota(ProviderQuota { + credits: None, plan_type: None, observed_at: Some(Utc::now()), refresh_token_expires_at: None, @@ -2681,6 +2688,7 @@ async fn accounts_list_should_not_attach_account_usage_to_model_specific_quota_w let provider = FakeProviderAdmin::new("openai", events()); let reset_at = Utc::now() + TimeDelta::days(7); provider.set_quota(ProviderQuota { + credits: None, plan_type: None, observed_at: Some(Utc::now()), refresh_token_expires_at: None, @@ -2982,6 +2990,7 @@ pub(super) fn document() -> ProviderDocument { fn empty_quota() -> ProviderQuota { ProviderQuota { + credits: None, plan_type: None, observed_at: None, refresh_token_expires_at: None, diff --git a/backend/crates/gateway-admin/tests/use_case/proxies.rs b/backend/crates/gateway-admin/tests/use_case/proxies.rs index b7de1f0f2..399ebb8c4 100644 --- a/backend/crates/gateway-admin/tests/use_case/proxies.rs +++ b/backend/crates/gateway-admin/tests/use_case/proxies.rs @@ -190,6 +190,7 @@ async fn linked_accounts_share_plan_resolution_and_only_read_cached_quota() { ] { let provider = FakeProviderAdmin::new("openai", events()); provider.set_quota(ProviderQuota { + credits: None, plan_type: cached.map(str::to_owned), observed_at: None, refresh_token_expires_at: None, diff --git a/backend/crates/gateway-api/src/admin/accounts/presenter.rs b/backend/crates/gateway-api/src/admin/accounts/presenter.rs index c816eeb01..752a3852c 100644 --- a/backend/crates/gateway-api/src/admin/accounts/presenter.rs +++ b/backend/crates/gateway-api/src/admin/accounts/presenter.rs @@ -220,6 +220,11 @@ pub(super) fn account_quota_view( rate_limit_reason, recovery_probe_required, windows, + credits: quota.credits.map(|credits| AccountQuotaCreditsView { + has_credits: credits.has_credits, + unlimited: credits.unlimited, + balance: credits.balance, + }), }, refresh_token_expires_at, ) diff --git a/backend/crates/gateway-api/src/admin/accounts/wire.rs b/backend/crates/gateway-api/src/admin/accounts/wire.rs index 937c2c6fc..4d10f29b1 100644 --- a/backend/crates/gateway-api/src/admin/accounts/wire.rs +++ b/backend/crates/gateway-api/src/admin/accounts/wire.rs @@ -397,6 +397,16 @@ pub struct AccountQuotaView { pub rate_limit_reason: Option, pub recovery_probe_required: bool, pub windows: Vec, + pub credits: Option, +} + +/// 上游点数安全视图,不透出额度响应中的其他 Provider 字段。 +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AccountQuotaCreditsView { + pub has_credits: bool, + pub unlimited: bool, + pub balance: Option, } /// 一个 quota 时间窗口。 diff --git a/backend/crates/gateway-plugin/runtime/tests/generation/data.rs b/backend/crates/gateway-plugin/runtime/tests/generation/data.rs index 777a64455..81eec737c 100644 --- a/backend/crates/gateway-plugin/runtime/tests/generation/data.rs +++ b/backend/crates/gateway-plugin/runtime/tests/generation/data.rs @@ -115,6 +115,7 @@ impl PluginAccountAccess for Facts { self.0.fetch_add(1, Ordering::SeqCst); assert_eq!(account.as_str(), "acct_facts"); Ok(ProviderQuota { + credits: None, plan_type: Some("private plan".into()), observed_at: None, refresh_token_expires_at: None, diff --git a/backend/crates/gateway-protocol/src/openai/events.rs b/backend/crates/gateway-protocol/src/openai/events.rs index 7384dfed2..6cccaec87 100644 --- a/backend/crates/gateway-protocol/src/openai/events.rs +++ b/backend/crates/gateway-protocol/src/openai/events.rs @@ -766,7 +766,8 @@ fn parse_credits_from_lookup(headers: &BTreeMap) -> Option Option { +/// 从额度查询或流式事件中提取同一份点数事实,缺失字段不推断为零余额。 +pub fn parse_credits_from_object(value: &Value) -> Option { Some(CreditsSnapshot { has_credits: value.get("has_credits")?.as_bool()?, unlimited: value.get("unlimited")?.as_bool()?, diff --git a/backend/crates/providers/openai/src/admin.rs b/backend/crates/providers/openai/src/admin.rs index e858eb301..34a7714ed 100644 --- a/backend/crates/providers/openai/src/admin.rs +++ b/backend/crates/providers/openai/src/admin.rs @@ -24,9 +24,9 @@ use gateway_admin::model::provider_credentials::{ ProviderExportCredentialInput, ProviderModel, ProviderModelCatalogDocument, ProviderModels, ProviderProfileActivityInsights, ProviderProfileAvatar, ProviderProfileAvatarStreamError, ProviderProfileDailyUsage, ProviderProfileInvocation, ProviderProfileStatistics, - ProviderProfileStatisticsSummary, ProviderQuota, ProviderQuotaRequest, ProviderQuotaWindow, - ProviderQuotaWindowRole, ProviderResetCredit, ProviderResetCreditResult, ProviderResetCredits, - ProviderSubscription, QuotaLocalUsageAttribution, + ProviderProfileStatisticsSummary, ProviderQuota, ProviderQuotaCredits, ProviderQuotaRequest, + ProviderQuotaWindow, ProviderQuotaWindowRole, ProviderResetCredit, ProviderResetCreditResult, + ProviderResetCredits, ProviderSubscription, QuotaLocalUsageAttribution, }; use gateway_admin::model::quota_forecast_sampling::QuotaForecastObservation; use gateway_admin::ports::provider::{ProviderAdmin, ProviderAdminError, ProviderAdminErrorKind}; @@ -1109,6 +1109,7 @@ fn account_matches_record(account: &ProviderAccount, record: &AccountRecord) -> fn empty_quota() -> ProviderQuota { ProviderQuota { + credits: None, plan_type: None, observed_at: None, refresh_token_expires_at: None, @@ -1174,6 +1175,11 @@ fn project_quota_snapshot(snapshot: CodexAccountQuotaSnapshot) -> ProviderQuota observed_at: Some(DateTime::::from(snapshot.observed_at())), refresh_token_expires_at: None, windows, + credits: snapshot.credits().map(|credits| ProviderQuotaCredits { + has_credits: credits.has_credits, + unlimited: credits.unlimited, + balance: credits.balance.clone(), + }), limit_reached, provider_data: Some(ProviderDocument::new(OpaqueProviderData::new( provider_data, diff --git a/backend/crates/providers/openai/src/credential/quota/mod.rs b/backend/crates/providers/openai/src/credential/quota/mod.rs index 5b9b9b228..ad934a9d0 100644 --- a/backend/crates/providers/openai/src/credential/quota/mod.rs +++ b/backend/crates/providers/openai/src/credential/quota/mod.rs @@ -1949,6 +1949,9 @@ fn merge_passive_metadata(quota: &mut Map, rate_limits: &ParsedRa value.insert("unlimited".to_owned(), Value::Bool(credits.unlimited)); if let Some(balance) = credits.balance.as_ref() { value.insert("balance".to_owned(), Value::String(balance.clone())); + } else { + // 点数对象明确更新但未提供余额时,不能把旧余额继续作为当前余额展示。 + value.remove("balance"); } quota.insert("credits".to_owned(), Value::Object(value)); } diff --git a/backend/crates/providers/openai/src/credential/quota/snapshot.rs b/backend/crates/providers/openai/src/credential/quota/snapshot.rs index 3ae020263..468f97300 100644 --- a/backend/crates/providers/openai/src/credential/quota/snapshot.rs +++ b/backend/crates/providers/openai/src/credential/quota/snapshot.rs @@ -10,6 +10,7 @@ use gateway_core::account::{ AccountQuotaSignals, CredentialRevision, ProviderAccountId, QuotaEvidence, QuotaObservation, QuotaState, }; +use gateway_protocol::openai::events::{CreditsSnapshot, parse_credits_from_object}; use serde_json::{Map, Value}; use super::document::{ @@ -114,6 +115,7 @@ pub struct CodexAccountQuotaSnapshot { credential_revision: CredentialRevision, observed_at: SystemTime, plan_type: Option, + credits: Option, fact: CodexQuotaFact, quota: QuotaState, windows: Vec, @@ -142,6 +144,12 @@ impl CodexAccountQuotaSnapshot { self.plan_type.as_deref() } + /// 点数只用于展示,不改变套餐限额或账号可用性。 + #[must_use] + pub fn credits(&self) -> Option<&CreditsSnapshot> { + self.credits.as_ref() + } + #[must_use] pub const fn fact(&self) -> CodexQuotaFact { self.fact @@ -350,6 +358,7 @@ pub(crate) fn parse_account_quota_snapshot( .get("plan_type") .and_then(Value::as_str) .map(str::to_owned), + credits: object.get("credits").and_then(parse_credits_from_object), fact, quota, windows, diff --git a/backend/crates/providers/openai/tests/admin.rs b/backend/crates/providers/openai/tests/admin.rs index 9478b6239..929f6af19 100644 --- a/backend/crates/providers/openai/tests/admin.rs +++ b/backend/crates/providers/openai/tests/admin.rs @@ -889,6 +889,85 @@ async fn openai_admin_quota_refresh_updates_the_account_plan() { } } +#[tokio::test] +async fn openai_admin_quota_projects_credit_balance_from_refresh_and_cached_observation() { + let store = Arc::new(MemoryAccountStore::default()); + store + .seed_oauth_credential(ImportCodexOAuthCredential { + account_id: "acct_credit_balance".to_owned(), + name: "credit balance".to_owned(), + secret: secret("credit-balance-test-token"), + verified_account: profile("chatgpt-credit-balance"), + next_refresh_at: None, + enabled: true, + }) + .await; + let account = store.account("acct_credit_balance").unwrap(); + let server = MockServer::start().await; + let mut config = valid_config(); + config.config.api.base_url = server.uri(); + let bundle = provider_openai::initialize( + config.config, + provider_ports_with(store.clone(), Arc::new(TestOAuthPending::default())), + ) + .await + .unwrap(); + for (wire, expected) in [ + ( + json!({"has_credits": true, "unlimited": false, "balance": "62500"}), + Some((true, false, Some("62500"))), + ), + ( + json!({"has_credits": false, "unlimited": false, "balance": 0}), + Some((false, false, Some("0"))), + ), + ( + json!({"has_credits": true, "unlimited": false, "balance": "9007199254740993.1234567890"}), + Some((true, false, Some("9007199254740993.1234567890"))), + ), + ( + json!({"has_credits": true, "unlimited": false, "balance": null}), + Some((true, false, None)), + ), + ( + json!({"has_credits": false, "unlimited": true}), + Some((false, true, None)), + ), + (Value::Null, None), + ] { + let _mock = Mock::given(method("GET")) + .and(path("/api/codex/usage")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "rate_limit": {"allowed": true, "primary_window": {"used_percent": 28}}, + "credits": wire, + }))) + .expect(1) + .mount_as_scoped(&server) + .await; + for refresh in [true, false] { + let quota = bundle + .admin_provider() + .quota(ProviderQuotaRequest { + account_id: account.id().clone(), + refresh, + rolling_usage: None, + }) + .await + .expect("quota with credits"); + assert_eq!( + quota.credits.as_ref().map(|credits| ( + credits.has_credits, + credits.unlimited, + credits.balance.as_deref(), + )), + expected + ); + assert_eq!(quota.windows[0].used_percent, Some(28.0)); + assert!(!quota.limit_reached); + } + } +} + #[tokio::test] async fn openai_admin_projects_free_plan_from_cached_quota_when_account_claims_omit_it() { let store = Arc::new(MemoryAccountStore::default()); diff --git a/backend/crates/providers/openai/tests/credential/quota/mod.rs b/backend/crates/providers/openai/tests/credential/quota/mod.rs index 4255952de..e7a4f5088 100644 --- a/backend/crates/providers/openai/tests/credential/quota/mod.rs +++ b/backend/crates/providers/openai/tests/credential/quota/mod.rs @@ -791,3 +791,40 @@ async fn quota_refresh_preserves_disabled_and_credential_error_facts() { AccountStatus::Disabled ); } + +#[tokio::test] +async fn passive_credit_updates_replace_balance_without_changing_quota_access() { + let store = Arc::new(MemoryAccountStore::default()); + create_account(&store, "acct_passive_credits").await; + let account = store.account("acct_passive_credits").unwrap(); + let service = quota_service(&store); + let initial = parse_rate_limits_event(&json!({ + "type": "codex.rate_limits", + "rate_limits": {"primary": {"used_percent": 28, "window_minutes": 300}}, + "credits": {"has_credits": true, "unlimited": false, "balance": "62500"} + })) + .unwrap(); + service + .synchronize_passive_rate_limits(&account, &[initial]) + .await + .unwrap(); + for (wire, expected) in [ + ( + json!({"has_credits": false, "unlimited": false, "balance": "0"}), + Some("0"), + ), + (json!({"has_credits": true, "unlimited": false}), None), + ] { + let update = + parse_rate_limits_event(&json!({"type": "codex.rate_limits", "credits": wire})) + .unwrap(); + service + .synchronize_passive_rate_limits(&account, &[update]) + .await + .unwrap(); + let snapshot = service.read_account(account.id()).await.unwrap().unwrap(); + assert_eq!(snapshot.credits().unwrap().balance.as_deref(), expected); + assert_eq!(snapshot.windows()[0].used_percent(), Some(28.0)); + assert_eq!(snapshot.quota().access(), QuotaAccessState::Allowed); + } +} diff --git a/backend/crates/providers/xai/src/admin.rs b/backend/crates/providers/xai/src/admin.rs index 3fbb27a6c..61421c6f7 100644 --- a/backend/crates/providers/xai/src/admin.rs +++ b/backend/crates/providers/xai/src/admin.rs @@ -1153,6 +1153,7 @@ fn project_quota( ) -> ProviderQuota { let Some(snapshot) = snapshot else { return ProviderQuota { + credits: None, plan_type: None, observed_at: None, refresh_token_expires_at, @@ -1225,6 +1226,7 @@ fn project_quota( } }; ProviderQuota { + credits: None, plan_type: billing.plan_type().map(str::to_owned), observed_at: Some(snapshot.observed_at()), refresh_token_expires_at, diff --git a/docs/api.md b/docs/api.md index 702fdd72a..da6e69918 100644 --- a/docs/api.md +++ b/docs/api.md @@ -556,6 +556,11 @@ Codex 的 `model_catalog_json` 配置。账号设置保存不等待上游模型 此时 `rateLimitedUntil` 是最早探测时间,到期后仍保持 `rate_limited`,直到探测成功或手动恢复。 未要求探测时,该字段表示冷却结束时间。所有此类情况统一显示“限流中”,仅详情原因和恢复条件不同 +`quota.credits` 返回上游点数余额,未提供点数信息时为 `null`。对象包含 `hasCredits`、`unlimited` 和 +`balance`,余额为保留上游精度的十进制字符串,未提供余额时为 `null`,明确的零余额为 `"0"`。 +OpenAI 点数随现有额度刷新和正常请求的额度信息同步,不与主动重置卡库存或百分比限额合并, +不参与账号可用性判断 + 账号列表和详情返回 `notes`(无备注时为 `null`)。编辑时省略或 `null` 保留原备注;字符串最多 500 个 Unicode 字符,允许换行和制表符,保存时去除首尾空白,空字符串清空备注。备注独立于上游身份,导入时未显式提供备注、 重新授权、凭据刷新及批量调度更新均保留已有备注 diff --git a/frontend/src/api/modules/accounts.ts b/frontend/src/api/modules/accounts.ts index 0935df689..e6cdbf82b 100644 --- a/frontend/src/api/modules/accounts.ts +++ b/frontend/src/api/modules/accounts.ts @@ -28,6 +28,12 @@ export interface AccountQuotaWindow { resetAtDisplay: string } +export interface AccountQuotaCredits { + hasCredits: boolean + unlimited: boolean + balance: string | null +} + export interface AccountQuota { refreshedAtDisplay: string limitReached: boolean @@ -36,6 +42,7 @@ export interface AccountQuota { rateLimitReason: 'upstream_rate_limit' | 'capacity_freeze' | null recoveryProbeRequired: boolean windows: AccountQuotaWindow[] + credits: AccountQuotaCredits | null } export interface AccountCurrencyCost { diff --git a/frontend/src/views/accounts/components/AccountQuotaPanel/Credits.vue b/frontend/src/views/accounts/components/AccountQuotaPanel/Credits.vue new file mode 100644 index 000000000..fe4c81078 --- /dev/null +++ b/frontend/src/views/accounts/components/AccountQuotaPanel/Credits.vue @@ -0,0 +1,45 @@ + + + diff --git a/frontend/src/views/accounts/components/AccountQuotaPanel/ResetCredits.vue b/frontend/src/views/accounts/components/AccountQuotaPanel/ResetCredits.vue index d124a5292..292e4e853 100644 --- a/frontend/src/views/accounts/components/AccountQuotaPanel/ResetCredits.vue +++ b/frontend/src/views/accounts/components/AccountQuotaPanel/ResetCredits.vue @@ -7,6 +7,7 @@ import dayjs from 'dayjs' import utc from 'dayjs/plugin/utc' import { computed, shallowRef, watch } from 'vue' import { useAccountResetCredits } from '../../composables/useAccountResetCredits' +import AccountQuotaCredits from './Credits.vue' import UsageLimits from './UsageLimits.vue' const props = defineProps<{ @@ -118,7 +119,7 @@ function handleRequestConsume(creditId: string) { :title="triggerLabel" @click="panelOpen = true" > - + x{{ availableCount }} @@ -247,6 +248,8 @@ function handleRequestConsume(creditId: string) { /> + +