From 9112817ec1552dfe7843e9c142c4e03288e7b9ad Mon Sep 17 00:00:00 2001 From: Nithish-KV Date: Wed, 29 Jul 2026 17:49:55 +0530 Subject: [PATCH 1/3] [NET-1411] fix: Capture usage for openai response API when stream ends due token limit --- netra/instrumentation/openai/wrappers.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/netra/instrumentation/openai/wrappers.py b/netra/instrumentation/openai/wrappers.py index 3d013f5f..a3b77de0 100644 --- a/netra/instrumentation/openai/wrappers.py +++ b/netra/instrumentation/openai/wrappers.py @@ -410,7 +410,8 @@ def _process_chunk(self, chunk: Any) -> None: {"message": {"role": "assistant", "content": assistant_text}} ] - usage = response.get("usage", {}) + # Capture usage even if status is not "completed" (early break due to token limit) + if (usage := response.get("usage", None)) is not None: self._complete_response["usage"] = usage self._span.add_event("llm.content.completion.chunk") @@ -560,7 +561,8 @@ def _process_chunk(self, chunk: Any) -> None: {"message": {"role": "assistant", "content": assistant_text}} ] - usage = response.get("usage", {}) + # Capture usage even if status is not "completed" (early break due to token limit) + if (usage := response.get("usage", None)) is not None: self._complete_response["usage"] = usage self._span.add_event("llm.content.completion.chunk") From 3dabe457271b0296f2d073a4e972bc6d72b6a64d Mon Sep 17 00:00:00 2001 From: Nithish-KV Date: Wed, 29 Jul 2026 18:00:27 +0530 Subject: [PATCH 2/3] [NET-1411] chore: Update changelog and library version --- CHANGELOG.md | 6 ++++-- netra/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef615a71..1146bb8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file. The format is based on Keep a Changelog and this project adheres to Semantic Versioning. -## [0.1.97b1] - 2026-07-28 +## [0.1.97] - - **Label evaluation/simulation traces on the root span** - Root spans produced by evaluation test runs (`Netra.evaluation`) and simulation runs (`Netra.simulation`) now carry a `netra.trace.origin` attribute set to `evaluation`, letting the backend and frontend distinguish these traces from normal workflow invocations. @@ -24,6 +24,8 @@ The format is based on Keep a Changelog and this project adheres to Semantic Ver - **Refactor stream wrapper architecture to use callback injection** - `stream_utils` is now a pure utility module with no Netra-internal imports. The commit logic (serialize and set attribute on root span) is injected as a callback from `SessionManager`, eliminating the circular dependency between `stream_utils` and `SessionManager`. +- **Fix OpenAI Responses API stream handling** — `Response API` instrumentation spans now include token usage when a stream ends due to reaching the token limit. + ## [0.1.96] - 2026-07-23 - **Reparent children of blocked root instruments instead of dropping the subtree** - When an instrumentation is not allowed to emit root-level spans, its children are now re-parented onto the nearest valid ancestor rather than dropping the entire subtree, so downstream spans are preserved. @@ -336,4 +338,4 @@ Users can be now overwrite the input and ouput attributes of spans created by in - Added utility to set input and output data for any active span in a trace -[0.1.97b1]: https://github.com/KeyValueSoftwareSystems/netra-sdk-py/tree/main +[0.1.97]: https://github.com/KeyValueSoftwareSystems/netra-sdk-py/tree/main diff --git a/netra/version.py b/netra/version.py index 1b6f4db9..a1eb4c95 100644 --- a/netra/version.py +++ b/netra/version.py @@ -1 +1 @@ -__version__ = "0.1.97b1" +__version__ = "0.1.97dev1" diff --git a/pyproject.toml b/pyproject.toml index af628bd1..038d7828 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [project] name = "netra-sdk" -version = "0.1.97b1" +version = "0.1.97dev1" description = "A Python SDK for AI application observability that provides OpenTelemetry-based monitoring, tracing, and PII protection for LLM and vector database applications. Enables easy instrumentation, session tracking, and privacy-focused data collection for AI systems in production environments." authors = [ {name = "Sooraj Thomas",email = "sooraj@keyvalue.systems"} From 97cc4e42c6dafec8ab79c63607ded5996a581fac Mon Sep 17 00:00:00 2001 From: Nithish-KV Date: Thu, 30 Jul 2026 11:42:28 +0530 Subject: [PATCH 3/3] [NET-1411] fix: Capture partial response when stream exhausts due to token limit --- CHANGELOG.md | 2 +- netra/instrumentation/openai/wrappers.py | 16 ++++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1146bb8a..5278cc81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,7 @@ The format is based on Keep a Changelog and this project adheres to Semantic Ver - **Refactor stream wrapper architecture to use callback injection** - `stream_utils` is now a pure utility module with no Netra-internal imports. The commit logic (serialize and set attribute on root span) is injected as a callback from `SessionManager`, eliminating the circular dependency between `stream_utils` and `SessionManager`. -- **Fix OpenAI Responses API stream handling** — `Response API` instrumentation spans now include token usage when a stream ends due to reaching the token limit. +- **Fix OpenAI Responses API stream handling** — `Response API` instrumentation spans now include token usage and completion output when a stream ends due to reaching the token limit (`incomplete` status). ## [0.1.96] - 2026-07-23 diff --git a/netra/instrumentation/openai/wrappers.py b/netra/instrumentation/openai/wrappers.py index a3b77de0..dcf204eb 100644 --- a/netra/instrumentation/openai/wrappers.py +++ b/netra/instrumentation/openai/wrappers.py @@ -397,7 +397,10 @@ def _process_chunk(self, chunk: Any) -> None: record_span_timing(self._span, RELATIVE_TIME_TO_FIRST_TOKEN, first_token_time, use_root_span=True) if chunk_dict.get("response"): response = chunk_dict.get("response", {}) - if response.get("status") == "completed": + if response.get("status") in ("completed", "incomplete"): + if response.get("model"): + self._complete_response["model"] = response["model"] + response_output = response.get("output") or [] for output in response_output: if output.get("type") == "function_call": @@ -410,8 +413,7 @@ def _process_chunk(self, chunk: Any) -> None: {"message": {"role": "assistant", "content": assistant_text}} ] - # Capture usage even if status is not "completed" (early break due to token limit) - if (usage := response.get("usage", None)) is not None: + usage = response.get("usage", {}) self._complete_response["usage"] = usage self._span.add_event("llm.content.completion.chunk") @@ -548,7 +550,10 @@ def _process_chunk(self, chunk: Any) -> None: record_span_timing(self._span, RELATIVE_TIME_TO_FIRST_TOKEN, first_token_time, use_root_span=True) if chunk_dict.get("response"): response = chunk_dict.get("response", {}) - if response.get("status") == "completed": + if response.get("status") in ("completed", "incomplete"): + if response.get("model"): + self._complete_response["model"] = response["model"] + response_output = response.get("output") or [] for output in response_output: if output.get("type") == "function_call": @@ -561,8 +566,7 @@ def _process_chunk(self, chunk: Any) -> None: {"message": {"role": "assistant", "content": assistant_text}} ] - # Capture usage even if status is not "completed" (early break due to token limit) - if (usage := response.get("usage", None)) is not None: + usage = response.get("usage", {}) self._complete_response["usage"] = usage self._span.add_event("llm.content.completion.chunk")