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 @@
+
+
+
+
+ 额度点数
+
+
+ {{ value }}
+
+ 点
+
+
+
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) {
/>
+
+
diff --git a/frontend/src/views/accounts/components/AccountQuotaPanel/index.vue b/frontend/src/views/accounts/components/AccountQuotaPanel/index.vue
index d41089e56..819ea598e 100644
--- a/frontend/src/views/accounts/components/AccountQuotaPanel/index.vue
+++ b/frontend/src/views/accounts/components/AccountQuotaPanel/index.vue
@@ -48,7 +48,7 @@ watch(hasPersonalInfo, (available) => {
v-if="account.capabilities.quota || quotaEntries.length > 0"
class="m-0 mt-1 flex min-w-0 items-center gap-1.5 text-cp-xs font-emphasis text-cp-text-secondary"
>
- {{ formatProviderLabel(account.provider) }} 额度
+ {{ account.provider === 'openai' ? 'Codex' : formatProviderLabel(account.provider) }} 额度
·
@@ -57,7 +57,7 @@ watch(hasPersonalInfo, (available) => {
最近刷新: {{ account.quota.refreshedAtDisplay }}
-
+
{
:pressed="forecastOpen"
@click="forecastOpen = true"
>
-
+