fix(proxy): patch P0-P3 routing/lifecycle issues across forwarder paths

* stream_check: thread Result from get_auth_headers via map_err so
  the workspace builds again
* forwarder: scope rectifier / budget-rectifier flags per-provider so
  failover can still apply rectification on the next attempt
* forwarder: categorize before record_result; route NonRetryable and
  ClientAbort through release_permit_neutral so client-side failures
  don't pollute circuit breaker or DB health
* handler_context: parse Gemini model from uri.path() and strip both
  ?query and :action verb defensively in extract_gemini_model_from_path
* forwarder + response_processor + handlers: introduce
  ActiveConnectionGuard (RAII) so active_connections decrement covers
  the full streaming body lifetime, not just response headers
* claude_desktop_config: use sort_by_key to clear the clippy gate
This commit is contained in:
Jason
2026-05-14 09:23:21 +08:00
parent 85131d37d8
commit 206125b4e3
6 changed files with 188 additions and 47 deletions
+24 -4
View File
@@ -175,10 +175,9 @@ impl RequestContext {
/// Gemini API 的模型名称在 URI 中,格式如:
/// `/v1beta/models/gemini-pro:generateContent`
pub fn with_model_from_uri(mut self, uri: &axum::http::Uri) -> Self {
let endpoint = uri
.path_and_query()
.map(|pq| pq.as_str())
.unwrap_or(uri.path());
// 用 path() 而不是 path_and_query():模型名必须从路径段中解析,
// 否则 GET /v1beta/models/<id>?key=... 会把 query 拼到 request_model 上。
let endpoint = uri.path();
self.request_model =
extract_gemini_model_from_path(endpoint).unwrap_or_else(|| "unknown".to_string());
@@ -285,6 +284,8 @@ pub(crate) fn extract_gemini_model_from_path(endpoint: &str) -> Option<String> {
.iter()
.position(|s| *s == "models")
.and_then(|i| segments.get(i + 1).copied())
// 防御性裁剪:即便调用方传入带 ? 或 :action 的字符串,也只保留 model id 本身
.map(|s| s.split('?').next().unwrap_or(s))
.map(|s| s.split(':').next().unwrap_or(s))
.filter(|s| !s.is_empty())
.map(|s| s.to_string())
@@ -347,4 +348,23 @@ mod tests {
// `/v1beta/models` (list endpoint) has no following segment → None.
assert_eq!(extract_gemini_model_from_path("/v1beta/models"), None);
}
#[test]
fn extract_model_get_with_query_only() {
// GET /v1beta/models/<id>?key=... 无 action verb,仅靠 ':' 拆分会把 query 带进 model 名。
// 修复后应该把 query 剥掉。
assert_eq!(
extract_gemini_model_from_path("/v1beta/models/gemini-pro?key=abc").as_deref(),
Some("gemini-pro"),
);
}
#[test]
fn extract_model_get_with_proxy_prefix_and_query() {
assert_eq!(
extract_gemini_model_from_path("/gemini/v1beta/models/gemini-2.0-flash?key=abc")
.as_deref(),
Some("gemini-2.0-flash"),
);
}
}