Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 23 additions & 3 deletions crates/tw-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -589,7 +589,11 @@ pub const MSG_CODES: &str = include_str!("../msg-codes.txt");
/// ChatGPT 登录的结果([`ChatgptLoginStatus`])不再带 `plan`,换成 `account`:和
/// [`OAuthView::account`] 同一块(邮箱、套餐),从同一个 access token 里读。照 21 写的
/// 界面会把 `/summary/routes` 当成数组去读,在登录结果里找不到套餐。
pub const CONTROL_API_VERSION: u32 = 22;
///
/// **23 起额度也来自 GLM Coding Plan**(Z.ai / BigModel 的上游):窗口多了 `monthly`,
/// 积分制套餐的窗口带上 [`QuotaWindow::credits`](总额、已用、剩余)。照 22 写的界面
/// 不认 `monthly`,也看不到剩余积分。
pub const CONTROL_API_VERSION: u32 = 23;

#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
Expand Down Expand Up @@ -1232,14 +1236,15 @@ pub struct RoutingView {
pub attempts: Vec<AttemptView>,
}

/// 一个订阅额度窗口。**每个字段都直接来自上游的响应头。**
/// 一个订阅额度窗口。**每个字段都直接来自上游**:响应头,或者账号的额度接口。
///
/// 我们自己推断的东西不放进这个结构 —— 界面上必须能区分「上游说的」和
/// 「我们猜的」,而混在一个类型里就区分不了了。
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
pub struct QuotaWindow {
/// `5h` / `7d`(Anthropic)/ `weekly`(Codex)
/// `5h` / `7d`(Anthropic)/ `weekly`(Codex、GLM)/ `monthly`(GLM 老套餐每月的
/// MCP 调用次数)
pub window: String,
pub used_percent: f64,
/// 什么时候重置,Unix 毫秒。**是时刻,不是「还有多少秒」**:上游报的秒数
Expand All @@ -1249,6 +1254,21 @@ pub struct QuotaWindow {
pub resets_at_ms: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub status: Option<String>,
/// 积分制套餐(GLM Coding Plan)这个窗口的积分。别的套餐没有
#[serde(default, skip_serializing_if = "Option::is_none")]
pub credits: Option<QuotaCredits>,
}

/// 一个额度窗口的积分,三个数都是上游给的原数。
///
/// **剩余不是总额减已用算出来的**:上游给的三个数不一定对得上(实测总额 2000、已用 23、
/// 剩余 1976),界面要显示剩余就显示它说的剩余。
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
pub struct QuotaCredits {
pub total: f64,
pub used: f64,
pub remaining: f64,
}

/// 一次调用的用量。
Expand Down
2 changes: 2 additions & 0 deletions crates/tw-control/src/chatgpt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -998,6 +998,7 @@ async fn usage(
used_percent: w.used_percent,
resets_at_ms: w.resets_at_ms,
status: w.status.clone(),
credits: None,
})
.collect(),
},
Expand All @@ -1022,6 +1023,7 @@ fn parse_usage(v: &Value, now_ms: u64) -> tw_api::ChatgptUsage {
.and_then(|x| x.as_u64())
.map(|secs| now_ms.saturating_add(secs.saturating_mul(1000))),
status: (used >= 100.0).then(|| "rejected".to_string()),
credits: None,
})
};
let limits = &v["rate_limit"];
Expand Down
18 changes: 8 additions & 10 deletions crates/tw-control/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -984,29 +984,27 @@ async fn request_detail(
///
/// 按量付费的账号没有这些头,那时这个列表是空的 —— 界面据此决定显示
/// 金额还是百分比,两种人格共用同一块地方。
///
/// GLM Coding Plan 的额度不在响应头里:**界面来要时去问**(60 秒内合成一次),问得慢的
/// 不等,问完由 `QuotaSeen` 补上
async fn quota(State(s): State<ControlState>) -> Json<Vec<tw_api::ProviderQuota>> {
s.gateway.refresh_glm_quotas(GLM_QUOTA_WAIT).await;
let mut out: Vec<tw_api::ProviderQuota> = s
.gateway
.quotas()
.into_iter()
.map(|(provider, q)| tw_api::ProviderQuota {
provider,
windows: q
.windows
.into_iter()
.map(|w| tw_api::QuotaWindow {
window: w.window,
used_percent: w.used_percent,
resets_at_ms: w.resets_at_ms,
status: w.status,
})
.collect(),
windows: q.windows.iter().map(Into::into).collect(),
})
.collect();
out.sort_by(|a, b| a.provider.cmp(&b.provider));
Json(out)
}

/// 界面来要额度时,最多等 GLM 的额度接口多久
const GLM_QUOTA_WAIT: std::time::Duration = std::time::Duration::from_secs(5);

/// 按上游分的延迟。**「哪家 TTFT 最差」问的是这个。**
///
/// 和按模型分是两个问题:前者的下一步是换上游,后者是换模型。
Expand Down
Loading
Loading