From 4131a8e7f2c98dc3e5559f11e2158ee2e7e9a64c Mon Sep 17 00:00:00 2001 From: Von Date: Fri, 13 Mar 2026 01:40:57 +0800 Subject: [PATCH] =?UTF-8?q?-=20=E4=BB=A3=E7=90=86=E5=8F=91=E7=BB=99?= =?UTF-8?q?=E4=B8=8A=E6=B8=B8=E6=97=B6=EF=BC=8C=E4=BB=8D=E7=84=B6=E4=BC=9A?= =?UTF-8?q?=E8=A1=A5=20stream=5Foptions.include=5Fusage=3Dtrue=EF=BC=8C?= =?UTF-8?q?=E6=89=80=E4=BB=A5=20token=20=E7=BB=9F=E8=AE=A1=E8=BF=98?= =?UTF-8?q?=E5=9C=A8=E3=80=82=20-=20=E4=B8=8B=E6=B8=B8=E5=A6=82=E6=9E=9C?= =?UTF-8?q?=E6=B2=A1=E6=9C=89=E6=98=BE=E5=BC=8F=E4=BC=A0=20stream=5Foption?= =?UTF-8?q?s.include=5Fusage=3Dtrue=EF=BC=8C=E4=BB=A3=E7=90=86=E4=BC=9A?= =?UTF-8?q?=E6=8A=8A=E2=80=9C=E7=BA=AF=20usage=20=E7=9A=84=20SSE=20chunk?= =?UTF-8?q?=E2=80=9D=E5=90=9E=E6=8E=89=EF=BC=8C=E4=B8=8D=E5=86=8D=E5=BE=80?= =?UTF-8?q?=E5=AE=A2=E6=88=B7=E7=AB=AF=E5=8F=91=E3=80=82=20-=20=E5=A6=82?= =?UTF-8?q?=E6=9E=9C=E4=B8=8B=E6=B8=B8=E6=9C=AC=E6=9D=A5=E5=B0=B1=E6=98=BE?= =?UTF-8?q?=E5=BC=8F=E8=A6=81=E6=B1=82=20include=5Fusage=3Dtrue=EF=BC=8C?= =?UTF-8?q?=E9=82=A3=E4=BB=8D=E7=84=B6=E4=BC=9A=E5=8E=9F=E6=A0=B7=E9=80=8F?= =?UTF-8?q?=E4=BC=A0=20usage=20chunk=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/external/response_adapter.py | 11 +++++++++++ src/presentation/proxy_controller.py | 9 ++++++++- src/services/proxy_service.py | 2 ++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/external/response_adapter.py b/src/external/response_adapter.py index 4517a26..1ef7f5d 100644 --- a/src/external/response_adapter.py +++ b/src/external/response_adapter.py @@ -21,6 +21,7 @@ def build_proxy_response( stream_context_func: Any, logger: Logger, on_complete: Optional[Callable[[Dict[str, Any]], None]] = None, + forward_stream_usage: bool = False, ) -> Response: """基于上游响应与输出钩子构建 Flask 响应。""" hook_func = getattr(hook, "output_body_hook", None) if hook else None @@ -62,6 +63,14 @@ def build_proxy_response( return None return stripped[5:].strip() + def is_usage_only_stream_chunk(payload: Any) -> bool: + if not isinstance(payload, dict): + return False + if not isinstance(payload.get("usage"), dict): + return False + choices = payload.get("choices") + return choices is None or (isinstance(choices, list) and len(choices) == 0) + def process_sse_event(event_text: str) -> Iterator[bytes]: # Preserve non-data SSE fields to avoid breaking strict clients. normalized_text = event_text.replace("\r\n", "\n").replace("\r", "\n") @@ -97,6 +106,8 @@ def build_proxy_response( if isinstance(data, dict): update_meta_from_payload(data) + if not forward_stream_usage and is_usage_only_stream_chunk(data): + return if hook_func: modified = hook_func(ctx, data) diff --git a/src/presentation/proxy_controller.py b/src/presentation/proxy_controller.py index b65f92f..11f4fd5 100644 --- a/src/presentation/proxy_controller.py +++ b/src/presentation/proxy_controller.py @@ -71,6 +71,9 @@ class ProxyController: ) stream_options = request_data.get("stream_options") + client_requested_usage_chunk = isinstance(stream_options, dict) and ( + stream_options.get("include_usage") is True + ) if not isinstance(stream_options, dict): stream_options = {} if stream_options.get("include_usage") is not True: @@ -109,7 +112,11 @@ class ProxyController: ) result, status_code = self._proxy_service.proxy_request( - provider, request_data, headers, on_complete=on_proxy_complete + provider, + request_data, + headers, + on_complete=on_proxy_complete, + forward_stream_usage=client_requested_usage_chunk, ) if result is None: self._logger.error(f"Proxy failed after retries: model={model_name!r}, ip={client_ip}") diff --git a/src/services/proxy_service.py b/src/services/proxy_service.py index 3812acd..654c7ac 100644 --- a/src/services/proxy_service.py +++ b/src/services/proxy_service.py @@ -30,6 +30,7 @@ class ProxyService: request_data: Dict[str, Any], request_headers: Dict[str, str], on_complete: Optional[Callable[[Dict[str, Any]], None]] = None, + forward_stream_usage: bool = False, ) -> Tuple[Optional[Response], int]: """代理请求到目标 provider,并处理重试与输出钩子。""" target_url = provider.api @@ -123,6 +124,7 @@ class ProxyService: stream_with_context, self._logger, on_complete=on_complete, + forward_stream_usage=forward_stream_usage, ) self._logger.info( "Upstream request completed: provider=%s status=%s stream=%s",