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
+16 -4
View File
@@ -3,6 +3,7 @@
//! 统一处理流式和非流式 API 响应
use super::{
forwarder::ActiveConnectionGuard,
handler_config::{StreamUsageEventFilter, UsageParserConfig},
handler_context::{RequestContext, StreamingTimeoutConfig},
hyper_client::ProxyResponse,
@@ -181,6 +182,7 @@ pub async fn handle_streaming(
ctx: &RequestContext,
state: &ProxyState,
parser_config: &UsageParserConfig,
connection_guard: Option<ActiveConnectionGuard>,
) -> Response {
let status = response.status();
log::debug!(
@@ -218,8 +220,13 @@ pub async fn handle_streaming(
let timeout_config = ctx.streaming_timeout_config();
// 创建带日志和超时的透传流
let logged_stream =
create_logged_passthrough_stream(stream, ctx.tag, usage_collector, timeout_config);
let logged_stream = create_logged_passthrough_stream(
stream,
ctx.tag,
usage_collector,
timeout_config,
connection_guard,
);
let body = axum::body::Body::from_stream(logged_stream);
match builder.body(body) {
@@ -237,6 +244,8 @@ pub async fn handle_non_streaming(
ctx: &RequestContext,
state: &ProxyState,
parser_config: &UsageParserConfig,
// guard 在函数 scope 内持有,整包响应读取完成后随函数返回一并 drop
_connection_guard: Option<ActiveConnectionGuard>,
) -> Result<Response, ProxyError> {
// 整包超时:仅在故障转移开启且配置值非零时生效
let body_timeout =
@@ -339,11 +348,12 @@ pub async fn process_response(
ctx: &RequestContext,
state: &ProxyState,
parser_config: &UsageParserConfig,
connection_guard: Option<ActiveConnectionGuard>,
) -> Result<Response, ProxyError> {
if is_sse_response(&response) {
Ok(handle_streaming(response, ctx, state, parser_config).await)
Ok(handle_streaming(response, ctx, state, parser_config, connection_guard).await)
} else {
handle_non_streaming(response, ctx, state, parser_config).await
handle_non_streaming(response, ctx, state, parser_config, connection_guard).await
}
}
@@ -631,8 +641,10 @@ pub fn create_logged_passthrough_stream(
tag: &'static str,
usage_collector: Option<SseUsageCollector>,
timeout_config: StreamingTimeoutConfig,
connection_guard: Option<ActiveConnectionGuard>,
) -> impl Stream<Item = Result<Bytes, std::io::Error>> + Send {
async_stream::stream! {
let _conn_guard = connection_guard;
let mut buffer = String::new();
let mut utf8_remainder: Vec<u8> = Vec::new();
let mut collector = usage_collector;