From 5a3050211475eb6bc7e8bd209e8ed834c9963636 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Mon, 23 Mar 2026 15:20:41 -0700 Subject: [PATCH] fix(openai): add context manager support to traced stream wrappers _TracedStream and _AsyncTracedStream lacked __enter__/__exit__ and __aenter__/__aexit__ methods, causing failures when callers used the OpenAI SDK's HTTP/2 streaming path which goes through LegacyAPIResponse.parse() and expects a context-manager-compatible stream. Also replace AsyncResponseWrapper and bare generator returns with _AsyncTracedStream/_TracedStream on all streaming paths so the wrapper type is consistent regardless of HTTP version. Adds a dedicated nox session (test_openai_http2_streaming) and regression tests covering sync/async context manager usage with h2. --- py/noxfile.py | 10 + py/src/braintrust/oai.py | 33 ++- ...tp2_context_manager_preserves_wrapper.yaml | 271 ++++++++++++++++++ ...tp2_context_manager_preserves_wrapper.yaml | 271 ++++++++++++++++++ ...sync_http2_preserves_stream_interface.yaml | 271 ++++++++++++++++++ py/src/braintrust/wrappers/test_openai.py | 8 +- .../braintrust/wrappers/test_openai_http2.py | 178 ++++++++++++ 7 files changed, 1033 insertions(+), 9 deletions(-) create mode 100644 py/src/braintrust/wrappers/cassettes/test_openai_chat_streaming_async_http2_context_manager_preserves_wrapper.yaml create mode 100644 py/src/braintrust/wrappers/cassettes/test_openai_chat_streaming_sync_http2_context_manager_preserves_wrapper.yaml create mode 100644 py/src/braintrust/wrappers/cassettes/test_openai_chat_streaming_sync_http2_preserves_stream_interface.yaml create mode 100644 py/src/braintrust/wrappers/test_openai_http2.py diff --git a/py/noxfile.py b/py/noxfile.py index 4c25af1f..ea07c82a 100644 --- a/py/noxfile.py +++ b/py/noxfile.py @@ -210,6 +210,16 @@ def test_openai(session, version): _run_core_tests(session) +@nox.session() +def test_openai_http2_streaming(session): + _install_test_deps(session) + _install(session, "openai") + # h2 is isolated to this session because it's only needed to force the + # HTTP/2 LegacyAPIResponse streaming path used by the regression test. + session.install("h2") + _run_tests(session, f"{WRAPPER_DIR}/test_openai_http2.py") + + @nox.session() def test_openrouter(session): """Test wrap_openai with OpenRouter. Requires OPENROUTER_API_KEY env var.""" diff --git a/py/src/braintrust/oai.py b/py/src/braintrust/oai.py index be8c3b17..735a1e76 100644 --- a/py/src/braintrust/oai.py +++ b/py/src/braintrust/oai.py @@ -19,10 +19,13 @@ class NamedWrapper: def __init__(self, wrapped: Any): + self._wrapped = wrapped + # Keep the legacy mangled attribute for existing wrapped-client checks + # that introspect `_NamedWrapper__wrapped` directly. self.__wrapped = wrapped def __getattr__(self, name: str) -> Any: - return getattr(self.__wrapped, name) + return getattr(self._wrapped, name) class AsyncResponseWrapper: @@ -188,7 +191,7 @@ def gen(): span.end() should_end = False - return gen() + return _TracedStream(raw_response, gen()) else: log_response = _try_to_dict(raw_response) metrics = _parse_metrics_from_usage(log_response.get("usage", {})) @@ -244,7 +247,7 @@ async def gen(): should_end = False streamer = gen() - return AsyncResponseWrapper(streamer) + return _AsyncTracedStream(raw_response, streamer) else: log_response = _try_to_dict(raw_response) metrics = _parse_metrics_from_usage(log_response.get("usage")) @@ -365,6 +368,16 @@ def __iter__(self) -> Any: def __next__(self) -> Any: return next(self._traced_generator) + def __enter__(self) -> Any: + if hasattr(self._wrapped, "__enter__"): + self._wrapped.__enter__() + return self + + def __exit__(self, exc_type, exc_val, exc_tb) -> Any: + if hasattr(self._wrapped, "__exit__"): + return self._wrapped.__exit__(exc_type, exc_val, exc_tb) + return None + class _AsyncTracedStream(NamedWrapper): """Traced async stream. Iterates via the traced generator while delegating @@ -380,6 +393,16 @@ def __aiter__(self) -> Any: async def __anext__(self) -> Any: return await self._traced_generator.__anext__() + async def __aenter__(self) -> Any: + if hasattr(self._wrapped, "__aenter__"): + await self._wrapped.__aenter__() + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb) -> Any: + if hasattr(self._wrapped, "__aexit__"): + return await self._wrapped.__aexit__(exc_type, exc_val, exc_tb) + return None + class _RawResponseWithTracedStream(NamedWrapper): """Proxy for LegacyAPIResponse that replaces parse() with a traced stream, @@ -445,7 +468,7 @@ def gen(): should_end = False if self.return_raw and hasattr(create_response, "parse"): return _RawResponseWithTracedStream(create_response, _TracedStream(raw_response, gen())) - return gen() + return _TracedStream(raw_response, gen()) else: log_response = _try_to_dict(raw_response) event_data = self._parse_event_from_result(log_response) @@ -498,7 +521,7 @@ async def gen(): streamer = gen() if self.return_raw and hasattr(create_response, "parse"): return _RawResponseWithTracedStream(create_response, _AsyncTracedStream(raw_response, streamer)) - return AsyncResponseWrapper(streamer) + return _AsyncTracedStream(raw_response, streamer) else: log_response = _try_to_dict(raw_response) event_data = self._parse_event_from_result(log_response) diff --git a/py/src/braintrust/wrappers/cassettes/test_openai_chat_streaming_async_http2_context_manager_preserves_wrapper.yaml b/py/src/braintrust/wrappers/cassettes/test_openai_chat_streaming_async_http2_context_manager_preserves_wrapper.yaml new file mode 100644 index 00000000..f517919b --- /dev/null +++ b/py/src/braintrust/wrappers/cassettes/test_openai_chat_streaming_async_http2_context_manager_preserves_wrapper.yaml @@ -0,0 +1,271 @@ +interactions: + - request: + body: '{"messages":[{"role":"user","content":"What''s 12 + 12?"}],"model":"gpt-4o-mini","stream":true,"stream_options":{"include_usage":true}}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - "134" + content-type: + - application/json + host: + - api.openai.com + user-agent: + - AsyncOpenAI/Python 1.82.0 + x-stainless-arch: + - arm64 + x-stainless-async: + - async:asyncio + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 1.82.0 + x-stainless-read-timeout: + - "600" + x-stainless-retry-count: + - "0" + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.13.3 + method: POST + uri: https://api.openai.com/v1/chat/completions + response: + body: + string: + 'data: {"id":"chatcmpl-BcNw6be6DuhpHqDlc8nKm7x3iMAL6","object":"chat.completion.chunk","created":1748488174,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_54eb4bd693","choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}],"usage":null} + + + data: {"id":"chatcmpl-BcNw6be6DuhpHqDlc8nKm7x3iMAL6","object":"chat.completion.chunk","created":1748488174,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_54eb4bd693","choices":[{"index":0,"delta":{"content":"12"},"logprobs":null,"finish_reason":null}],"usage":null} + + + data: {"id":"chatcmpl-BcNw6be6DuhpHqDlc8nKm7x3iMAL6","object":"chat.completion.chunk","created":1748488174,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_54eb4bd693","choices":[{"index":0,"delta":{"content":" + +"},"logprobs":null,"finish_reason":null}],"usage":null} + + + data: {"id":"chatcmpl-BcNw6be6DuhpHqDlc8nKm7x3iMAL6","object":"chat.completion.chunk","created":1748488174,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_54eb4bd693","choices":[{"index":0,"delta":{"content":" + "},"logprobs":null,"finish_reason":null}],"usage":null} + + + data: {"id":"chatcmpl-BcNw6be6DuhpHqDlc8nKm7x3iMAL6","object":"chat.completion.chunk","created":1748488174,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_54eb4bd693","choices":[{"index":0,"delta":{"content":"12"},"logprobs":null,"finish_reason":null}],"usage":null} + + + data: {"id":"chatcmpl-BcNw6be6DuhpHqDlc8nKm7x3iMAL6","object":"chat.completion.chunk","created":1748488174,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_54eb4bd693","choices":[{"index":0,"delta":{"content":" + equals"},"logprobs":null,"finish_reason":null}],"usage":null} + + + data: {"id":"chatcmpl-BcNw6be6DuhpHqDlc8nKm7x3iMAL6","object":"chat.completion.chunk","created":1748488174,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_54eb4bd693","choices":[{"index":0,"delta":{"content":" + "},"logprobs":null,"finish_reason":null}],"usage":null} + + + data: {"id":"chatcmpl-BcNw6be6DuhpHqDlc8nKm7x3iMAL6","object":"chat.completion.chunk","created":1748488174,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_54eb4bd693","choices":[{"index":0,"delta":{"content":"24"},"logprobs":null,"finish_reason":null}],"usage":null} + + + data: {"id":"chatcmpl-BcNw6be6DuhpHqDlc8nKm7x3iMAL6","object":"chat.completion.chunk","created":1748488174,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_54eb4bd693","choices":[{"index":0,"delta":{"content":"."},"logprobs":null,"finish_reason":null}],"usage":null} + + + data: {"id":"chatcmpl-BcNw6be6DuhpHqDlc8nKm7x3iMAL6","object":"chat.completion.chunk","created":1748488174,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_54eb4bd693","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}],"usage":null} + + + data: {"id":"chatcmpl-BcNw6be6DuhpHqDlc8nKm7x3iMAL6","object":"chat.completion.chunk","created":1748488174,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_54eb4bd693","choices":[],"usage":{"prompt_tokens":14,"completion_tokens":8,"total_tokens":22,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}}} + + + data: [DONE] + + + ' + headers: + CF-RAY: + - 9472cb30e8d5e56c-EWR + Connection: + - keep-alive + Content-Type: + - text/event-stream; charset=utf-8 + Date: + - Thu, 29 May 2025 03:09:34 GMT + Server: + - cloudflare + Set-Cookie: + - __cf_bm=AIq4yjX3ru.J9gwNR6zSYLQNorConUZa5qtJ6wXxuvE-1748488174-1.0.1.1-KquMaoYitsL5z76ow2IPzasSn98mtC1_QEt9VOT1pvvQt_obPUDugNtsEGJCc_wP50_X4wP.kC7nYuf98KX8dCPpiq2ZqY5vwVCdgocqRxU; + path=/; expires=Thu, 29-May-25 03:39:34 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=o0LrLIiV.VvLFX1H1bbtbV01AjzSfXrfrVn0fU7pANY-1748488174648-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + access-control-expose-headers: + - X-Request-ID + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-organization: + - braintrust-data + openai-processing-ms: + - "313" + openai-version: + - "2020-10-01" + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + x-envoy-upstream-service-time: + - "317" + x-ratelimit-limit-requests: + - "30000" + x-ratelimit-limit-tokens: + - "150000000" + x-ratelimit-remaining-requests: + - "29999" + x-ratelimit-remaining-tokens: + - "149999993" + x-ratelimit-reset-requests: + - 2ms + x-ratelimit-reset-tokens: + - 0s + x-request-id: + - req_7718454117290f001d635e2ea50bf0b5 + status: + code: 200 + message: OK + - request: + body: '{"messages":[{"role":"user","content":"What''s 12 + 12?"}],"model":"gpt-4o-mini","stream":true,"stream_options":{"include_usage":true}}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - "134" + content-type: + - application/json + cookie: + - __cf_bm=AIq4yjX3ru.J9gwNR6zSYLQNorConUZa5qtJ6wXxuvE-1748488174-1.0.1.1-KquMaoYitsL5z76ow2IPzasSn98mtC1_QEt9VOT1pvvQt_obPUDugNtsEGJCc_wP50_X4wP.kC7nYuf98KX8dCPpiq2ZqY5vwVCdgocqRxU; + _cfuvid=o0LrLIiV.VvLFX1H1bbtbV01AjzSfXrfrVn0fU7pANY-1748488174648-0.0.1.1-604800000 + host: + - api.openai.com + user-agent: + - AsyncOpenAI/Python 1.82.0 + x-stainless-arch: + - arm64 + x-stainless-async: + - async:asyncio + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 1.82.0 + x-stainless-raw-response: + - "true" + x-stainless-read-timeout: + - "600" + x-stainless-retry-count: + - "0" + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.13.3 + method: POST + uri: https://api.openai.com/v1/chat/completions + response: + body: + string: + 'data: {"id":"chatcmpl-BcNw7TPkf33tHhtv5BBYd6zunIaPW","object":"chat.completion.chunk","created":1748488175,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_54eb4bd693","choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}],"usage":null} + + + data: {"id":"chatcmpl-BcNw7TPkf33tHhtv5BBYd6zunIaPW","object":"chat.completion.chunk","created":1748488175,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_54eb4bd693","choices":[{"index":0,"delta":{"content":"12"},"logprobs":null,"finish_reason":null}],"usage":null} + + + data: {"id":"chatcmpl-BcNw7TPkf33tHhtv5BBYd6zunIaPW","object":"chat.completion.chunk","created":1748488175,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_54eb4bd693","choices":[{"index":0,"delta":{"content":" + +"},"logprobs":null,"finish_reason":null}],"usage":null} + + + data: {"id":"chatcmpl-BcNw7TPkf33tHhtv5BBYd6zunIaPW","object":"chat.completion.chunk","created":1748488175,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_54eb4bd693","choices":[{"index":0,"delta":{"content":" + "},"logprobs":null,"finish_reason":null}],"usage":null} + + + data: {"id":"chatcmpl-BcNw7TPkf33tHhtv5BBYd6zunIaPW","object":"chat.completion.chunk","created":1748488175,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_54eb4bd693","choices":[{"index":0,"delta":{"content":"12"},"logprobs":null,"finish_reason":null}],"usage":null} + + + data: {"id":"chatcmpl-BcNw7TPkf33tHhtv5BBYd6zunIaPW","object":"chat.completion.chunk","created":1748488175,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_54eb4bd693","choices":[{"index":0,"delta":{"content":" + equals"},"logprobs":null,"finish_reason":null}],"usage":null} + + + data: {"id":"chatcmpl-BcNw7TPkf33tHhtv5BBYd6zunIaPW","object":"chat.completion.chunk","created":1748488175,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_54eb4bd693","choices":[{"index":0,"delta":{"content":" + "},"logprobs":null,"finish_reason":null}],"usage":null} + + + data: {"id":"chatcmpl-BcNw7TPkf33tHhtv5BBYd6zunIaPW","object":"chat.completion.chunk","created":1748488175,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_54eb4bd693","choices":[{"index":0,"delta":{"content":"24"},"logprobs":null,"finish_reason":null}],"usage":null} + + + data: {"id":"chatcmpl-BcNw7TPkf33tHhtv5BBYd6zunIaPW","object":"chat.completion.chunk","created":1748488175,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_54eb4bd693","choices":[{"index":0,"delta":{"content":"."},"logprobs":null,"finish_reason":null}],"usage":null} + + + data: {"id":"chatcmpl-BcNw7TPkf33tHhtv5BBYd6zunIaPW","object":"chat.completion.chunk","created":1748488175,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_54eb4bd693","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}],"usage":null} + + + data: {"id":"chatcmpl-BcNw7TPkf33tHhtv5BBYd6zunIaPW","object":"chat.completion.chunk","created":1748488175,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_54eb4bd693","choices":[],"usage":{"prompt_tokens":14,"completion_tokens":8,"total_tokens":22,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}}} + + + data: [DONE] + + + ' + headers: + CF-RAY: + - 9472cb34d8b0e56c-EWR + Connection: + - keep-alive + Content-Type: + - text/event-stream; charset=utf-8 + Date: + - Thu, 29 May 2025 03:09:35 GMT + Server: + - cloudflare + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + access-control-expose-headers: + - X-Request-ID + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-organization: + - braintrust-data + openai-processing-ms: + - "324" + openai-version: + - "2020-10-01" + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + x-envoy-upstream-service-time: + - "328" + x-ratelimit-limit-requests: + - "30000" + x-ratelimit-limit-tokens: + - "150000000" + x-ratelimit-remaining-requests: + - "29999" + x-ratelimit-remaining-tokens: + - "149999993" + x-ratelimit-reset-requests: + - 2ms + x-ratelimit-reset-tokens: + - 0s + x-request-id: + - req_8e14ac90b3fc7df08ab71458200c0b80 + status: + code: 200 + message: OK +version: 1 diff --git a/py/src/braintrust/wrappers/cassettes/test_openai_chat_streaming_sync_http2_context_manager_preserves_wrapper.yaml b/py/src/braintrust/wrappers/cassettes/test_openai_chat_streaming_sync_http2_context_manager_preserves_wrapper.yaml new file mode 100644 index 00000000..e666c5db --- /dev/null +++ b/py/src/braintrust/wrappers/cassettes/test_openai_chat_streaming_sync_http2_context_manager_preserves_wrapper.yaml @@ -0,0 +1,271 @@ +interactions: + - request: + body: '{"messages":[{"role":"user","content":"What''s 12 + 12?"}],"model":"gpt-4o-mini","stream":true,"stream_options":{"include_usage":true}}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - "134" + content-type: + - application/json + host: + - api.openai.com + user-agent: + - OpenAI/Python 1.82.0 + x-stainless-arch: + - arm64 + x-stainless-async: + - "false" + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 1.82.0 + x-stainless-read-timeout: + - "600" + x-stainless-retry-count: + - "0" + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.13.3 + method: POST + uri: https://api.openai.com/v1/chat/completions + response: + body: + string: + 'data: {"id":"chatcmpl-BcNvsUEkWF6Xk2kAAUSi21kgVYyER","object":"chat.completion.chunk","created":1748488160,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_62a23a81ef","choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}],"usage":null} + + + data: {"id":"chatcmpl-BcNvsUEkWF6Xk2kAAUSi21kgVYyER","object":"chat.completion.chunk","created":1748488160,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_62a23a81ef","choices":[{"index":0,"delta":{"content":"12"},"logprobs":null,"finish_reason":null}],"usage":null} + + + data: {"id":"chatcmpl-BcNvsUEkWF6Xk2kAAUSi21kgVYyER","object":"chat.completion.chunk","created":1748488160,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_62a23a81ef","choices":[{"index":0,"delta":{"content":" + +"},"logprobs":null,"finish_reason":null}],"usage":null} + + + data: {"id":"chatcmpl-BcNvsUEkWF6Xk2kAAUSi21kgVYyER","object":"chat.completion.chunk","created":1748488160,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_62a23a81ef","choices":[{"index":0,"delta":{"content":" + "},"logprobs":null,"finish_reason":null}],"usage":null} + + + data: {"id":"chatcmpl-BcNvsUEkWF6Xk2kAAUSi21kgVYyER","object":"chat.completion.chunk","created":1748488160,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_62a23a81ef","choices":[{"index":0,"delta":{"content":"12"},"logprobs":null,"finish_reason":null}],"usage":null} + + + data: {"id":"chatcmpl-BcNvsUEkWF6Xk2kAAUSi21kgVYyER","object":"chat.completion.chunk","created":1748488160,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_62a23a81ef","choices":[{"index":0,"delta":{"content":" + equals"},"logprobs":null,"finish_reason":null}],"usage":null} + + + data: {"id":"chatcmpl-BcNvsUEkWF6Xk2kAAUSi21kgVYyER","object":"chat.completion.chunk","created":1748488160,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_62a23a81ef","choices":[{"index":0,"delta":{"content":" + "},"logprobs":null,"finish_reason":null}],"usage":null} + + + data: {"id":"chatcmpl-BcNvsUEkWF6Xk2kAAUSi21kgVYyER","object":"chat.completion.chunk","created":1748488160,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_62a23a81ef","choices":[{"index":0,"delta":{"content":"24"},"logprobs":null,"finish_reason":null}],"usage":null} + + + data: {"id":"chatcmpl-BcNvsUEkWF6Xk2kAAUSi21kgVYyER","object":"chat.completion.chunk","created":1748488160,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_62a23a81ef","choices":[{"index":0,"delta":{"content":"."},"logprobs":null,"finish_reason":null}],"usage":null} + + + data: {"id":"chatcmpl-BcNvsUEkWF6Xk2kAAUSi21kgVYyER","object":"chat.completion.chunk","created":1748488160,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_62a23a81ef","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}],"usage":null} + + + data: {"id":"chatcmpl-BcNvsUEkWF6Xk2kAAUSi21kgVYyER","object":"chat.completion.chunk","created":1748488160,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_62a23a81ef","choices":[],"usage":{"prompt_tokens":14,"completion_tokens":8,"total_tokens":22,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}}} + + + data: [DONE] + + + ' + headers: + CF-RAY: + - 9472cadbaa3a6109-EWR + Connection: + - keep-alive + Content-Type: + - text/event-stream; charset=utf-8 + Date: + - Thu, 29 May 2025 03:09:21 GMT + Server: + - cloudflare + Set-Cookie: + - __cf_bm=TaY3Tidv7pN1rwXifsEbRpgiJBSYyZmtrqFVhID5fxU-1748488161-1.0.1.1-yge4Qd_h2Czeue_4iHBseCabVMKErLW_dzyoNMs9A2ATm_3je1fFN03F_YfpeLqu1yq_W7BFbR0dqQpI_OxzTmh2tTKXdQDjQLoJP5ivPHI; + path=/; expires=Thu, 29-May-25 03:39:21 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=hV6VthxryE_9X2dzgxNwp6TyFZV.fQvAIpOzE5TrGpU-1748488161396-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + access-control-expose-headers: + - X-Request-ID + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-organization: + - braintrust-data + openai-processing-ms: + - "681" + openai-version: + - "2020-10-01" + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + x-envoy-upstream-service-time: + - "686" + x-ratelimit-limit-requests: + - "30000" + x-ratelimit-limit-tokens: + - "150000000" + x-ratelimit-remaining-requests: + - "29999" + x-ratelimit-remaining-tokens: + - "149999993" + x-ratelimit-reset-requests: + - 2ms + x-ratelimit-reset-tokens: + - 0s + x-request-id: + - req_301ee2fbed0944b5203d0a79fc80645b + status: + code: 200 + message: OK + - request: + body: '{"messages":[{"role":"user","content":"What''s 12 + 12?"}],"model":"gpt-4o-mini","stream":true,"stream_options":{"include_usage":true}}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - "134" + content-type: + - application/json + cookie: + - __cf_bm=TaY3Tidv7pN1rwXifsEbRpgiJBSYyZmtrqFVhID5fxU-1748488161-1.0.1.1-yge4Qd_h2Czeue_4iHBseCabVMKErLW_dzyoNMs9A2ATm_3je1fFN03F_YfpeLqu1yq_W7BFbR0dqQpI_OxzTmh2tTKXdQDjQLoJP5ivPHI; + _cfuvid=hV6VthxryE_9X2dzgxNwp6TyFZV.fQvAIpOzE5TrGpU-1748488161396-0.0.1.1-604800000 + host: + - api.openai.com + user-agent: + - OpenAI/Python 1.82.0 + x-stainless-arch: + - arm64 + x-stainless-async: + - "false" + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 1.82.0 + x-stainless-raw-response: + - "true" + x-stainless-read-timeout: + - "600" + x-stainless-retry-count: + - "0" + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.13.3 + method: POST + uri: https://api.openai.com/v1/chat/completions + response: + body: + string: + 'data: {"id":"chatcmpl-BcNvt62bH7Bdi0CHQVKMIhuQTFiFp","object":"chat.completion.chunk","created":1748488161,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_62a23a81ef","choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}],"usage":null} + + + data: {"id":"chatcmpl-BcNvt62bH7Bdi0CHQVKMIhuQTFiFp","object":"chat.completion.chunk","created":1748488161,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_62a23a81ef","choices":[{"index":0,"delta":{"content":"12"},"logprobs":null,"finish_reason":null}],"usage":null} + + + data: {"id":"chatcmpl-BcNvt62bH7Bdi0CHQVKMIhuQTFiFp","object":"chat.completion.chunk","created":1748488161,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_62a23a81ef","choices":[{"index":0,"delta":{"content":" + +"},"logprobs":null,"finish_reason":null}],"usage":null} + + + data: {"id":"chatcmpl-BcNvt62bH7Bdi0CHQVKMIhuQTFiFp","object":"chat.completion.chunk","created":1748488161,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_62a23a81ef","choices":[{"index":0,"delta":{"content":" + "},"logprobs":null,"finish_reason":null}],"usage":null} + + + data: {"id":"chatcmpl-BcNvt62bH7Bdi0CHQVKMIhuQTFiFp","object":"chat.completion.chunk","created":1748488161,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_62a23a81ef","choices":[{"index":0,"delta":{"content":"12"},"logprobs":null,"finish_reason":null}],"usage":null} + + + data: {"id":"chatcmpl-BcNvt62bH7Bdi0CHQVKMIhuQTFiFp","object":"chat.completion.chunk","created":1748488161,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_62a23a81ef","choices":[{"index":0,"delta":{"content":" + equals"},"logprobs":null,"finish_reason":null}],"usage":null} + + + data: {"id":"chatcmpl-BcNvt62bH7Bdi0CHQVKMIhuQTFiFp","object":"chat.completion.chunk","created":1748488161,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_62a23a81ef","choices":[{"index":0,"delta":{"content":" + "},"logprobs":null,"finish_reason":null}],"usage":null} + + + data: {"id":"chatcmpl-BcNvt62bH7Bdi0CHQVKMIhuQTFiFp","object":"chat.completion.chunk","created":1748488161,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_62a23a81ef","choices":[{"index":0,"delta":{"content":"24"},"logprobs":null,"finish_reason":null}],"usage":null} + + + data: {"id":"chatcmpl-BcNvt62bH7Bdi0CHQVKMIhuQTFiFp","object":"chat.completion.chunk","created":1748488161,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_62a23a81ef","choices":[{"index":0,"delta":{"content":"."},"logprobs":null,"finish_reason":null}],"usage":null} + + + data: {"id":"chatcmpl-BcNvt62bH7Bdi0CHQVKMIhuQTFiFp","object":"chat.completion.chunk","created":1748488161,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_62a23a81ef","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}],"usage":null} + + + data: {"id":"chatcmpl-BcNvt62bH7Bdi0CHQVKMIhuQTFiFp","object":"chat.completion.chunk","created":1748488161,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_62a23a81ef","choices":[],"usage":{"prompt_tokens":14,"completion_tokens":8,"total_tokens":22,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}}} + + + data: [DONE] + + + ' + headers: + CF-RAY: + - 9472cae2792a6109-EWR + Connection: + - keep-alive + Content-Type: + - text/event-stream; charset=utf-8 + Date: + - Thu, 29 May 2025 03:09:21 GMT + Server: + - cloudflare + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + access-control-expose-headers: + - X-Request-ID + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-organization: + - braintrust-data + openai-processing-ms: + - "192" + openai-version: + - "2020-10-01" + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + x-envoy-upstream-service-time: + - "195" + x-ratelimit-limit-requests: + - "30000" + x-ratelimit-limit-tokens: + - "150000000" + x-ratelimit-remaining-requests: + - "29999" + x-ratelimit-remaining-tokens: + - "149999993" + x-ratelimit-reset-requests: + - 2ms + x-ratelimit-reset-tokens: + - 0s + x-request-id: + - req_2d19c07b9dca45eb4d8ba0070cf1d930 + status: + code: 200 + message: OK +version: 1 diff --git a/py/src/braintrust/wrappers/cassettes/test_openai_chat_streaming_sync_http2_preserves_stream_interface.yaml b/py/src/braintrust/wrappers/cassettes/test_openai_chat_streaming_sync_http2_preserves_stream_interface.yaml new file mode 100644 index 00000000..e666c5db --- /dev/null +++ b/py/src/braintrust/wrappers/cassettes/test_openai_chat_streaming_sync_http2_preserves_stream_interface.yaml @@ -0,0 +1,271 @@ +interactions: + - request: + body: '{"messages":[{"role":"user","content":"What''s 12 + 12?"}],"model":"gpt-4o-mini","stream":true,"stream_options":{"include_usage":true}}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - "134" + content-type: + - application/json + host: + - api.openai.com + user-agent: + - OpenAI/Python 1.82.0 + x-stainless-arch: + - arm64 + x-stainless-async: + - "false" + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 1.82.0 + x-stainless-read-timeout: + - "600" + x-stainless-retry-count: + - "0" + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.13.3 + method: POST + uri: https://api.openai.com/v1/chat/completions + response: + body: + string: + 'data: {"id":"chatcmpl-BcNvsUEkWF6Xk2kAAUSi21kgVYyER","object":"chat.completion.chunk","created":1748488160,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_62a23a81ef","choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}],"usage":null} + + + data: {"id":"chatcmpl-BcNvsUEkWF6Xk2kAAUSi21kgVYyER","object":"chat.completion.chunk","created":1748488160,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_62a23a81ef","choices":[{"index":0,"delta":{"content":"12"},"logprobs":null,"finish_reason":null}],"usage":null} + + + data: {"id":"chatcmpl-BcNvsUEkWF6Xk2kAAUSi21kgVYyER","object":"chat.completion.chunk","created":1748488160,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_62a23a81ef","choices":[{"index":0,"delta":{"content":" + +"},"logprobs":null,"finish_reason":null}],"usage":null} + + + data: {"id":"chatcmpl-BcNvsUEkWF6Xk2kAAUSi21kgVYyER","object":"chat.completion.chunk","created":1748488160,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_62a23a81ef","choices":[{"index":0,"delta":{"content":" + "},"logprobs":null,"finish_reason":null}],"usage":null} + + + data: {"id":"chatcmpl-BcNvsUEkWF6Xk2kAAUSi21kgVYyER","object":"chat.completion.chunk","created":1748488160,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_62a23a81ef","choices":[{"index":0,"delta":{"content":"12"},"logprobs":null,"finish_reason":null}],"usage":null} + + + data: {"id":"chatcmpl-BcNvsUEkWF6Xk2kAAUSi21kgVYyER","object":"chat.completion.chunk","created":1748488160,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_62a23a81ef","choices":[{"index":0,"delta":{"content":" + equals"},"logprobs":null,"finish_reason":null}],"usage":null} + + + data: {"id":"chatcmpl-BcNvsUEkWF6Xk2kAAUSi21kgVYyER","object":"chat.completion.chunk","created":1748488160,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_62a23a81ef","choices":[{"index":0,"delta":{"content":" + "},"logprobs":null,"finish_reason":null}],"usage":null} + + + data: {"id":"chatcmpl-BcNvsUEkWF6Xk2kAAUSi21kgVYyER","object":"chat.completion.chunk","created":1748488160,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_62a23a81ef","choices":[{"index":0,"delta":{"content":"24"},"logprobs":null,"finish_reason":null}],"usage":null} + + + data: {"id":"chatcmpl-BcNvsUEkWF6Xk2kAAUSi21kgVYyER","object":"chat.completion.chunk","created":1748488160,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_62a23a81ef","choices":[{"index":0,"delta":{"content":"."},"logprobs":null,"finish_reason":null}],"usage":null} + + + data: {"id":"chatcmpl-BcNvsUEkWF6Xk2kAAUSi21kgVYyER","object":"chat.completion.chunk","created":1748488160,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_62a23a81ef","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}],"usage":null} + + + data: {"id":"chatcmpl-BcNvsUEkWF6Xk2kAAUSi21kgVYyER","object":"chat.completion.chunk","created":1748488160,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_62a23a81ef","choices":[],"usage":{"prompt_tokens":14,"completion_tokens":8,"total_tokens":22,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}}} + + + data: [DONE] + + + ' + headers: + CF-RAY: + - 9472cadbaa3a6109-EWR + Connection: + - keep-alive + Content-Type: + - text/event-stream; charset=utf-8 + Date: + - Thu, 29 May 2025 03:09:21 GMT + Server: + - cloudflare + Set-Cookie: + - __cf_bm=TaY3Tidv7pN1rwXifsEbRpgiJBSYyZmtrqFVhID5fxU-1748488161-1.0.1.1-yge4Qd_h2Czeue_4iHBseCabVMKErLW_dzyoNMs9A2ATm_3je1fFN03F_YfpeLqu1yq_W7BFbR0dqQpI_OxzTmh2tTKXdQDjQLoJP5ivPHI; + path=/; expires=Thu, 29-May-25 03:39:21 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=hV6VthxryE_9X2dzgxNwp6TyFZV.fQvAIpOzE5TrGpU-1748488161396-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + access-control-expose-headers: + - X-Request-ID + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-organization: + - braintrust-data + openai-processing-ms: + - "681" + openai-version: + - "2020-10-01" + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + x-envoy-upstream-service-time: + - "686" + x-ratelimit-limit-requests: + - "30000" + x-ratelimit-limit-tokens: + - "150000000" + x-ratelimit-remaining-requests: + - "29999" + x-ratelimit-remaining-tokens: + - "149999993" + x-ratelimit-reset-requests: + - 2ms + x-ratelimit-reset-tokens: + - 0s + x-request-id: + - req_301ee2fbed0944b5203d0a79fc80645b + status: + code: 200 + message: OK + - request: + body: '{"messages":[{"role":"user","content":"What''s 12 + 12?"}],"model":"gpt-4o-mini","stream":true,"stream_options":{"include_usage":true}}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - "134" + content-type: + - application/json + cookie: + - __cf_bm=TaY3Tidv7pN1rwXifsEbRpgiJBSYyZmtrqFVhID5fxU-1748488161-1.0.1.1-yge4Qd_h2Czeue_4iHBseCabVMKErLW_dzyoNMs9A2ATm_3je1fFN03F_YfpeLqu1yq_W7BFbR0dqQpI_OxzTmh2tTKXdQDjQLoJP5ivPHI; + _cfuvid=hV6VthxryE_9X2dzgxNwp6TyFZV.fQvAIpOzE5TrGpU-1748488161396-0.0.1.1-604800000 + host: + - api.openai.com + user-agent: + - OpenAI/Python 1.82.0 + x-stainless-arch: + - arm64 + x-stainless-async: + - "false" + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 1.82.0 + x-stainless-raw-response: + - "true" + x-stainless-read-timeout: + - "600" + x-stainless-retry-count: + - "0" + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.13.3 + method: POST + uri: https://api.openai.com/v1/chat/completions + response: + body: + string: + 'data: {"id":"chatcmpl-BcNvt62bH7Bdi0CHQVKMIhuQTFiFp","object":"chat.completion.chunk","created":1748488161,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_62a23a81ef","choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}],"usage":null} + + + data: {"id":"chatcmpl-BcNvt62bH7Bdi0CHQVKMIhuQTFiFp","object":"chat.completion.chunk","created":1748488161,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_62a23a81ef","choices":[{"index":0,"delta":{"content":"12"},"logprobs":null,"finish_reason":null}],"usage":null} + + + data: {"id":"chatcmpl-BcNvt62bH7Bdi0CHQVKMIhuQTFiFp","object":"chat.completion.chunk","created":1748488161,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_62a23a81ef","choices":[{"index":0,"delta":{"content":" + +"},"logprobs":null,"finish_reason":null}],"usage":null} + + + data: {"id":"chatcmpl-BcNvt62bH7Bdi0CHQVKMIhuQTFiFp","object":"chat.completion.chunk","created":1748488161,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_62a23a81ef","choices":[{"index":0,"delta":{"content":" + "},"logprobs":null,"finish_reason":null}],"usage":null} + + + data: {"id":"chatcmpl-BcNvt62bH7Bdi0CHQVKMIhuQTFiFp","object":"chat.completion.chunk","created":1748488161,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_62a23a81ef","choices":[{"index":0,"delta":{"content":"12"},"logprobs":null,"finish_reason":null}],"usage":null} + + + data: {"id":"chatcmpl-BcNvt62bH7Bdi0CHQVKMIhuQTFiFp","object":"chat.completion.chunk","created":1748488161,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_62a23a81ef","choices":[{"index":0,"delta":{"content":" + equals"},"logprobs":null,"finish_reason":null}],"usage":null} + + + data: {"id":"chatcmpl-BcNvt62bH7Bdi0CHQVKMIhuQTFiFp","object":"chat.completion.chunk","created":1748488161,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_62a23a81ef","choices":[{"index":0,"delta":{"content":" + "},"logprobs":null,"finish_reason":null}],"usage":null} + + + data: {"id":"chatcmpl-BcNvt62bH7Bdi0CHQVKMIhuQTFiFp","object":"chat.completion.chunk","created":1748488161,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_62a23a81ef","choices":[{"index":0,"delta":{"content":"24"},"logprobs":null,"finish_reason":null}],"usage":null} + + + data: {"id":"chatcmpl-BcNvt62bH7Bdi0CHQVKMIhuQTFiFp","object":"chat.completion.chunk","created":1748488161,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_62a23a81ef","choices":[{"index":0,"delta":{"content":"."},"logprobs":null,"finish_reason":null}],"usage":null} + + + data: {"id":"chatcmpl-BcNvt62bH7Bdi0CHQVKMIhuQTFiFp","object":"chat.completion.chunk","created":1748488161,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_62a23a81ef","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}],"usage":null} + + + data: {"id":"chatcmpl-BcNvt62bH7Bdi0CHQVKMIhuQTFiFp","object":"chat.completion.chunk","created":1748488161,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_62a23a81ef","choices":[],"usage":{"prompt_tokens":14,"completion_tokens":8,"total_tokens":22,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}}} + + + data: [DONE] + + + ' + headers: + CF-RAY: + - 9472cae2792a6109-EWR + Connection: + - keep-alive + Content-Type: + - text/event-stream; charset=utf-8 + Date: + - Thu, 29 May 2025 03:09:21 GMT + Server: + - cloudflare + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + access-control-expose-headers: + - X-Request-ID + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-organization: + - braintrust-data + openai-processing-ms: + - "192" + openai-version: + - "2020-10-01" + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + x-envoy-upstream-service-time: + - "195" + x-ratelimit-limit-requests: + - "30000" + x-ratelimit-limit-tokens: + - "150000000" + x-ratelimit-remaining-requests: + - "29999" + x-ratelimit-remaining-tokens: + - "149999993" + x-ratelimit-reset-requests: + - 2ms + x-ratelimit-reset-tokens: + - 0s + x-request-id: + - req_2d19c07b9dca45eb4d8ba0070cf1d930 + status: + code: 200 + message: OK +version: 1 diff --git a/py/src/braintrust/wrappers/test_openai.py b/py/src/braintrust/wrappers/test_openai.py index 6ab9b343..eee7ecea 100644 --- a/py/src/braintrust/wrappers/test_openai.py +++ b/py/src/braintrust/wrappers/test_openai.py @@ -91,6 +91,10 @@ def test_openai_chat_metrics(memory_logger): metrics = span["metrics"] assert_metrics_are_valid(metrics, start, end) assert TEST_MODEL in span["metadata"]["model"] + # assert span["metadata"]["provider"] == "openai" + assert TEST_PROMPT in str(span["input"]) + assert "24" in str(span["output"]) or "twenty-four" in str(span["output"]).lower() + assert TEST_MODEL in span["metadata"]["model"] assert span["metadata"]["provider"] == "openai" assert TEST_PROMPT in str(span["input"]) @@ -458,10 +462,6 @@ def test_openai_chat_streaming_sync(memory_logger): assert span metrics = span["metrics"] assert_metrics_are_valid(metrics, start, end) - assert TEST_MODEL in span["metadata"]["model"] - # assert span["metadata"]["provider"] == "openai" - assert TEST_PROMPT in str(span["input"]) - assert "24" in str(span["output"]) or "twenty-four" in str(span["output"]).lower() @pytest.mark.vcr diff --git a/py/src/braintrust/wrappers/test_openai_http2.py b/py/src/braintrust/wrappers/test_openai_http2.py new file mode 100644 index 00000000..aeb3db5d --- /dev/null +++ b/py/src/braintrust/wrappers/test_openai_http2.py @@ -0,0 +1,178 @@ +import time + +import httpx +import openai +import pytest +from braintrust import logger, wrap_openai +from braintrust.test_helpers import init_test_logger +from braintrust.wrappers.test_utils import assert_metrics_are_valid +from openai import AsyncOpenAI + + +PROJECT_NAME = "test-project-openai-py-tracing" +TEST_MODEL = "gpt-4o-mini" +TEST_PROMPT = "What's 12 + 12?" + + +@pytest.fixture +def memory_logger(): + init_test_logger(PROJECT_NAME) + with logger._internal_with_memory_background_logger() as bgl: + yield bgl + + +@pytest.mark.vcr +def test_openai_chat_streaming_sync_http2_preserves_stream_interface(memory_logger): + assert not memory_logger.pop() + + unwrapped_client = openai.OpenAI(http_client=httpx.Client(http2=True)) + wrapped_client = wrap_openai(openai.OpenAI(http_client=httpx.Client(http2=True))) + clients = [(unwrapped_client, False), (wrapped_client, True)] + + try: + for client, wrapped in clients: + start = time.time() + + stream = client.chat.completions.create( + model=TEST_MODEL, + messages=[{"role": "user", "content": TEST_PROMPT}], + stream=True, + stream_options={"include_usage": True}, + ) + + # HTTP/2 pushes the OpenAI SDK through the LegacyAPIResponse.parse() + # path. The wrapped stream must still preserve the SDK stream surface. + assert hasattr(stream, "response") + assert hasattr(stream, "_iterator") + + chunks = [] + for chunk in stream: + chunks.append(chunk) + end = time.time() + + assert chunks + assert len(chunks) > 1 + + content = "" + for chunk in chunks: + if chunk.choices and chunk.choices[0].delta.content: + content += chunk.choices[0].delta.content + + assert "24" in content or "twenty-four" in content.lower() + + if not wrapped: + assert not memory_logger.pop() + continue + + spans = memory_logger.pop() + assert len(spans) == 1 + span = spans[0] + metrics = span["metrics"] + assert_metrics_are_valid(metrics, start, end) + assert TEST_MODEL in span["metadata"]["model"] + assert span["metadata"]["provider"] == "openai" + assert TEST_PROMPT in str(span["input"]) + assert "24" in str(span["output"]) or "twenty-four" in str(span["output"]).lower() + finally: + unwrapped_client.close() + wrapped_client.close() + + +@pytest.mark.vcr +def test_openai_chat_streaming_sync_http2_context_manager_preserves_wrapper(memory_logger): + assert not memory_logger.pop() + + client = wrap_openai(openai.OpenAI(http_client=httpx.Client(http2=True))) + + try: + start = time.time() + stream = client.chat.completions.create( + model=TEST_MODEL, + messages=[{"role": "user", "content": TEST_PROMPT}], + stream=True, + stream_options={"include_usage": True}, + ) + + assert hasattr(stream, "response") + assert hasattr(stream, "_iterator") + + chunks = [] + with stream as entered_stream: + assert entered_stream is stream + for chunk in entered_stream: + chunks.append(chunk) + end = time.time() + + assert chunks + assert len(chunks) > 1 + + content = "" + for chunk in chunks: + if chunk.choices and chunk.choices[0].delta.content: + content += chunk.choices[0].delta.content + + assert "24" in content or "twenty-four" in content.lower() + + spans = memory_logger.pop() + assert len(spans) == 1 + span = spans[0] + metrics = span["metrics"] + assert_metrics_are_valid(metrics, start, end) + assert TEST_MODEL in span["metadata"]["model"] + assert span["metadata"]["provider"] == "openai" + assert TEST_PROMPT in str(span["input"]) + finally: + client.close() + + +@pytest.mark.asyncio +@pytest.mark.vcr +async def test_openai_chat_streaming_async_http2_context_manager_preserves_wrapper(memory_logger): + assert not memory_logger.pop() + + client = wrap_openai(AsyncOpenAI(http_client=httpx.AsyncClient(http2=True))) + + try: + start = time.time() + stream = await client.chat.completions.create( + model=TEST_MODEL, + messages=[{"role": "user", "content": TEST_PROMPT}], + stream=True, + stream_options={"include_usage": True}, + ) + + assert hasattr(stream, "response") + assert hasattr(stream, "_iterator") + + chunks = [] + async with stream as entered_stream: + # The async chat wrapper still returns AsyncResponseWrapper at the + # outer boundary, while __aenter__ yields the traced inner stream. + # So we assert the traced stream interface is preserved rather than + # Python object identity with the outer wrapper. + assert entered_stream.response is stream.response + assert entered_stream._iterator is stream._iterator + async for chunk in entered_stream: + chunks.append(chunk) + end = time.time() + + assert chunks + assert len(chunks) > 1 + + content = "" + for chunk in chunks: + if chunk.choices and chunk.choices[0].delta.content: + content += chunk.choices[0].delta.content + + assert "24" in content or "twenty-four" in content.lower() + + spans = memory_logger.pop() + assert len(spans) == 1 + span = spans[0] + metrics = span["metrics"] + assert_metrics_are_valid(metrics, start, end) + assert TEST_MODEL in span["metadata"]["model"] + assert span["metadata"]["provider"] == "openai" + assert TEST_PROMPT in str(span["input"]) + finally: + await client.close()