mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 12:44:18 +08:00
fix(coding-plan): route Zhipu quota query to the user's configured base URL (#3702)
Fixes #3701. `query_zhipu` was hard-coded to `https://api.z.ai`, so a user who configured the mainland China preset (`Zhipu GLM` on `open.bigmodel.cn`) could not retrieve usage once the international endpoint became unreachable from their network (or vice versa). The two endpoints share the same quota path (`/api/monitor/usage/quota/limit`) and return JSON in the same shape, and — crucially — each user only ever uses one of them: the quota host is the same host they're already running coding on. So we can route by the configured `base_url` and skip the cross-host fallback entirely. What this PR changes -------------------- A single helper that maps the user's `base_url` to the matching quota host, and `query_zhipu` rebuilt to take `base_url` and pick the right host: fn zhipu_quota_base(base_url: &str) -> &'static str { if base_url.contains("bigmodel.cn") { "https://open.bigmodel.cn" } else { "https://api.z.ai" } } async fn query_zhipu(base_url: &str, api_key: &str) -> SubscriptionQuota { let url = format!( "{}/api/monitor/usage/quota/limit", zhipu_quota_base(base_url), ); // ... original 401/403 -> Expired / make_error / parse path, unchanged } The dispatcher already distinguishes `ZhipuCn` from `ZhipuEn` via `detect_provider()` and routes the call through `query_zhipu(base_url, api_key)` in the same match arm. Why no cross-host fallback -------------------------- Farion's review pointed out that adding a fallback would be over-engineered and actively harmful: 1. Reachability is determined by the preset the user chose. Their configured host is the host they are already using to run coding; if it were unreachable, the user could not have reached the "query usage" step at all. 2. The fallback path required distinguishing "both 401/403" (genuine bad key) from "one 401/403 + one network error" (regional block), which silently misclassified the second case as a generic query failure and hid the upstream "Session expired" UX for invalid keys. 3. It also cost the worst-case ~10s+10s≈20s serial timeout for users on a working primary. With the URL-based routing in place, 401/403 returns to the original `CredentialStatus::Expired` semantics — same UX as `query_kimi` and `query_minimax`. Files changed ------------- - `src-tauri/src/services/coding_plan.rs` — 1 file, +35 / -20 Testing ------- - 3 new `zhipu_quota_base_*` routing tests - 15 existing `coding_plan` parser tests still pass - `cargo fmt --check` clean - `cargo clippy --lib --no-deps -- -D warnings` clean Co-authored-by: Yongmao Luo <yongmao.luo@columbia.edu> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -239,12 +239,31 @@ fn parse_zhipu_token_tiers(data: &serde_json::Value) -> Vec<QuotaTier> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
async fn query_zhipu(api_key: &str) -> SubscriptionQuota {
|
||||
let client = crate::proxy::http_client::get();
|
||||
/// Resolve the Zhipu quota endpoint from the user's configured `base_url`.
|
||||
///
|
||||
/// Zhipu ships as two distinct presets (Zhipu GLM = `open.bigmodel.cn`,
|
||||
/// Zhipu GLM en = `api.z.ai`) that share the same quota path and JSON shape.
|
||||
/// The quota endpoint lives on the same host as the user's coding endpoint,
|
||||
/// so we route by `base_url` and let the caller's existing reachability
|
||||
/// (they're already using this host to run coding) determine success — no
|
||||
/// cross-host fallback, no auth-error heuristics.
|
||||
fn zhipu_quota_base(base_url: &str) -> &'static str {
|
||||
if base_url.to_lowercase().contains("bigmodel.cn") {
|
||||
"https://open.bigmodel.cn"
|
||||
} else {
|
||||
"https://api.z.ai"
|
||||
}
|
||||
}
|
||||
|
||||
async fn query_zhipu(base_url: &str, api_key: &str) -> SubscriptionQuota {
|
||||
let client = crate::proxy::http_client::get();
|
||||
let url = format!(
|
||||
"{}/api/monitor/usage/quota/limit",
|
||||
zhipu_quota_base(base_url)
|
||||
);
|
||||
|
||||
// 统一走 api.z.ai 国际站(中国站 bigmodel.cn 有反爬机制)
|
||||
let resp = client
|
||||
.get("https://api.z.ai/api/monitor/usage/quota/limit")
|
||||
.get(&url)
|
||||
.header("Authorization", api_key) // 注意:智谱不加 Bearer 前缀
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Accept-Language", "en-US,en")
|
||||
@@ -629,7 +648,9 @@ pub async fn get_coding_plan_quota(
|
||||
|
||||
let quota = match provider {
|
||||
CodingPlanProvider::Kimi => query_kimi(api_key).await,
|
||||
CodingPlanProvider::ZhipuCn | CodingPlanProvider::ZhipuEn => query_zhipu(api_key).await,
|
||||
CodingPlanProvider::ZhipuCn | CodingPlanProvider::ZhipuEn => {
|
||||
query_zhipu(base_url, api_key).await
|
||||
}
|
||||
CodingPlanProvider::MiniMaxCn => query_minimax(api_key, true).await,
|
||||
CodingPlanProvider::MiniMaxEn => query_minimax(api_key, false).await,
|
||||
CodingPlanProvider::ZenMux => query_zenmux(base_url, api_key).await,
|
||||
@@ -640,7 +661,10 @@ pub async fn get_coding_plan_quota(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{parse_minimax_tiers, parse_zhipu_token_tiers, TIER_FIVE_HOUR, TIER_WEEKLY_LIMIT};
|
||||
use super::{
|
||||
parse_minimax_tiers, parse_zhipu_token_tiers, zhipu_quota_base, TIER_FIVE_HOUR,
|
||||
TIER_WEEKLY_LIMIT,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
@@ -940,4 +964,42 @@ mod tests {
|
||||
assert_eq!(tiers[0].name, TIER_FIVE_HOUR);
|
||||
assert_eq!(tiers[0].utilization, 20.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zhipu_quota_base_routes_bigmodel_url_to_cn_endpoint() {
|
||||
assert_eq!(
|
||||
zhipu_quota_base("https://open.bigmodel.cn/api/paas/v4"),
|
||||
"https://open.bigmodel.cn"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zhipu_quota_base_routes_z_ai_url_to_en_endpoint() {
|
||||
assert_eq!(
|
||||
zhipu_quota_base("https://api.z.ai/api/paas/v4"),
|
||||
"https://api.z.ai"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zhipu_quota_base_defaults_to_en_for_unknown_url() {
|
||||
// 没有明显 Zhipu 域名特征时,默认走国际站(更通用的入口)
|
||||
assert_eq!(
|
||||
zhipu_quota_base("https://example.com/zhipu"),
|
||||
"https://api.z.ai"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zhipu_quota_base_routes_uppercase_cn_url_to_cn_endpoint() {
|
||||
// 大小写不敏感:与 detect_provider 保持一致的约定,避免大写 preset URL 静默路由到国际站
|
||||
assert_eq!(
|
||||
zhipu_quota_base("HTTPS://OPEN.BIGMODEL.CN/api/paas/v4"),
|
||||
"https://open.bigmodel.cn"
|
||||
);
|
||||
assert_eq!(
|
||||
zhipu_quota_base("https://Open.BigModel.cn/api/paas/v4"),
|
||||
"https://open.bigmodel.cn"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user