From ad030da3b1f8b437af52e81f221b3f1b21074858 Mon Sep 17 00:00:00 2001 From: Yongmao Luo <32817679+YongmaoLuo@users.noreply.github.com> Date: Thu, 4 Jun 2026 23:10:45 +0800 Subject: [PATCH] fix(coding-plan): route Zhipu quota query to the user's configured base URL (#3702) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Co-authored-by: Claude Opus 4.7 --- src-tauri/src/services/coding_plan.rs | 74 ++++++++++++++++++++++++--- 1 file changed, 68 insertions(+), 6 deletions(-) diff --git a/src-tauri/src/services/coding_plan.rs b/src-tauri/src/services/coding_plan.rs index 1798b10c5..39c3309f2 100644 --- a/src-tauri/src/services/coding_plan.rs +++ b/src-tauri/src/services/coding_plan.rs @@ -239,12 +239,31 @@ fn parse_zhipu_token_tiers(data: &serde_json::Value) -> Vec { .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" + ); + } }