From f33074651e5e5109a3c99f807933ba6d4203e0ff Mon Sep 17 00:00:00 2001 From: chienyuanchang Date: Fri, 10 Apr 2026 15:30:58 -0700 Subject: [PATCH 01/13] add usage --- .../contentunderstanding/aio/models/_patch.py | 25 +++- .../ai/contentunderstanding/models/_patch.py | 23 ++++ .../tests/test_analyzer_operation_id.py | 118 +++++++++++++++++- ...erstanding_content_analyzers_operations.py | 4 + ...ding_content_analyzers_operations_async.py | 4 + .../tests/test_helpers.py | 19 +++ 6 files changed, 191 insertions(+), 2 deletions(-) diff --git a/sdk/contentunderstanding/azure-ai-contentunderstanding/azure/ai/contentunderstanding/aio/models/_patch.py b/sdk/contentunderstanding/azure-ai-contentunderstanding/azure/ai/contentunderstanding/aio/models/_patch.py index eca66620c5d4..aad46cd7e183 100644 --- a/sdk/contentunderstanding/azure-ai-contentunderstanding/azure/ai/contentunderstanding/aio/models/_patch.py +++ b/sdk/contentunderstanding/azure-ai-contentunderstanding/azure/ai/contentunderstanding/aio/models/_patch.py @@ -10,8 +10,10 @@ """ import re -from typing import Any, TypeVar +from typing import Any, Optional, TypeVar from azure.core.polling import AsyncLROPoller, AsyncPollingMethod +from ... import models as _models +from ..._utils.model_base import _deserialize PollingReturnType_co = TypeVar("PollingReturnType_co", covariant=True) @@ -62,6 +64,27 @@ def operation_id(self) -> str: except (KeyError, ValueError) as e: raise ValueError(f"Could not extract operation ID: {str(e)}") from e + @property + def usage(self) -> Optional["_models.UsageDetails"]: + """Returns the usage details from the completed analyze operation. + + This property is available after the operation has completed successfully. + It provides information about the resources consumed during analysis, + including document pages, contextualization tokens, and LLM token breakdown. + + :return: The usage details, or None if the operation is not yet complete + or usage information is not available. + :rtype: ~azure.ai.contentunderstanding.models.UsageDetails or None + """ + try: + response = self.polling_method()._pipeline_response.http_response # type: ignore # pylint: disable=protected-access + usage_data = response.json().get("usage") + if usage_data is None: + return None + return _deserialize(_models.UsageDetails, usage_data) + except AttributeError: + return None + @classmethod def from_poller(cls, poller: AsyncLROPoller[PollingReturnType_co]) -> "AnalyzeAsyncLROPoller[PollingReturnType_co]": # pyright: ignore[reportInvalidTypeArguments] # fmt: skip """Wrap an existing AsyncLROPoller without re-initializing the polling method. diff --git a/sdk/contentunderstanding/azure-ai-contentunderstanding/azure/ai/contentunderstanding/models/_patch.py b/sdk/contentunderstanding/azure-ai-contentunderstanding/azure/ai/contentunderstanding/models/_patch.py index 9dee69387cba..3cf79f823e54 100644 --- a/sdk/contentunderstanding/azure-ai-contentunderstanding/azure/ai/contentunderstanding/models/_patch.py +++ b/sdk/contentunderstanding/azure-ai-contentunderstanding/azure/ai/contentunderstanding/models/_patch.py @@ -14,6 +14,8 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional, TypeVar from azure.core import CaseInsensitiveEnumMeta from azure.core.polling import LROPoller, PollingMethod +from . import _models +from .._utils.model_base import _deserialize from ._models import ( StringField, IntegerField, @@ -155,6 +157,27 @@ def operation_id(self) -> str: except (KeyError, ValueError) as e: raise ValueError(f"Could not extract operation ID: {str(e)}") from e + @property + def usage(self) -> Optional["_models.UsageDetails"]: + """Returns the usage details from the completed analyze operation. + + This property is available after the operation has completed successfully. + It provides information about the resources consumed during analysis, + including document pages, contextualization tokens, and LLM token breakdown. + + :return: The usage details, or None if the operation is not yet complete + or usage information is not available. + :rtype: ~azure.ai.contentunderstanding.models.UsageDetails or None + """ + try: + response = self.polling_method()._pipeline_response.http_response # type: ignore # pylint: disable=protected-access + usage_data = response.json().get("usage") + if usage_data is None: + return None + return _deserialize(_models.UsageDetails, usage_data) + except AttributeError: + return None + @classmethod def from_poller(cls, poller: LROPoller[PollingReturnType_co]) -> "AnalyzeLROPoller[PollingReturnType_co]": # pyright: ignore[reportInvalidTypeArguments] # fmt: skip """Wrap an existing LROPoller without re-initializing the polling method. diff --git a/sdk/contentunderstanding/azure-ai-contentunderstanding/tests/test_analyzer_operation_id.py b/sdk/contentunderstanding/azure-ai-contentunderstanding/tests/test_analyzer_operation_id.py index d3647016a6ae..6fc3f1ee1fdb 100644 --- a/sdk/contentunderstanding/azure-ai-contentunderstanding/tests/test_analyzer_operation_id.py +++ b/sdk/contentunderstanding/azure-ai-contentunderstanding/tests/test_analyzer_operation_id.py @@ -16,7 +16,7 @@ AnalyzeLROPoller, _parse_operation_id, ) -from azure.ai.contentunderstanding.models import AnalysisInput +from azure.ai.contentunderstanding.models import AnalysisInput, UsageDetails from azure.ai.contentunderstanding import ContentUnderstandingClient @@ -164,3 +164,119 @@ def test_analyze_operation_returns_custom_poller(self): assert hasattr(result, "operation_id") operation_id = result.operation_id assert operation_id == "test-op-id-123" + + +class TestAnalyzeLROPollerUsage: + """Test the usage property on AnalyzeLROPoller (GitHub issue #46249).""" + + def _make_poller_with_usage(self, usage_json): + """Helper to create an AnalyzeLROPoller with a mocked final response containing usage data.""" + mock_polling_method = Mock() + mock_initial_response = Mock() + mock_http_response = Mock() + mock_http_response.headers = { + "Operation-Location": "https://endpoint/contentunderstanding/analyzerResults/test-op-id?api-version=2025-11-01" + } + mock_initial_response.http_response = mock_http_response + mock_polling_method.return_value = mock_polling_method + mock_polling_method._initial_response = mock_initial_response + + # Mock the final pipeline response (used by .usage property) + mock_final_response = Mock() + mock_final_http_response = Mock() + response_json = { + "id": "test-op-id", + "status": "Succeeded", + "result": {"contents": []}, + } + if usage_json is not None: + response_json["usage"] = usage_json + mock_final_http_response.json.return_value = response_json + mock_final_response.http_response = mock_final_http_response + mock_polling_method._pipeline_response = mock_final_response + + return AnalyzeLROPoller( + client=Mock(), + initial_response=Mock(), + deserialization_callback=Mock(), + polling_method=mock_polling_method, + ) + + def test_usage_with_full_data(self): + """Test usage property returns UsageDetails with all fields populated.""" + usage_json = { + "documentPagesMinimal": 0, + "documentPagesBasic": 0, + "documentPagesStandard": 2, + "audioHours": 0.0, + "videoHours": 0.0, + "contextualizationTokens": 1500, + "tokens": { + "gpt-4.1-input": 500, + "gpt-4.1-output": 200, + "text-embedding-3-large-input": 300, + }, + } + + poller = self._make_poller_with_usage(usage_json) + usage = poller.usage + + assert usage is not None + assert isinstance(usage, UsageDetails) + assert usage.document_pages_standard == 2 + assert usage.document_pages_minimal == 0 + assert usage.document_pages_basic == 0 + assert usage.contextualization_tokens == 1500 + assert usage.tokens == { + "gpt-4.1-input": 500, + "gpt-4.1-output": 200, + "text-embedding-3-large-input": 300, + } + + def test_usage_with_partial_data(self): + """Test usage property with only some fields populated.""" + usage_json = { + "documentPagesStandard": 1, + "contextualizationTokens": 100, + } + + poller = self._make_poller_with_usage(usage_json) + usage = poller.usage + + assert usage is not None + assert usage.document_pages_standard == 1 + assert usage.contextualization_tokens == 100 + assert usage.document_pages_minimal is None + assert usage.tokens is None + + def test_usage_returns_none_when_not_present(self): + """Test usage property returns None when usage is not in the response.""" + poller = self._make_poller_with_usage(None) + usage = poller.usage + + assert usage is None + + def test_usage_returns_none_before_completion(self): + """Test usage property returns None when the polling has not yet completed.""" + mock_polling_method = Mock() + mock_initial_response = Mock() + mock_http_response = Mock() + mock_http_response.headers = { + "Operation-Location": "https://endpoint/contentunderstanding/analyzerResults/test-op-id?api-version=2025-11-01" + } + mock_initial_response.http_response = mock_http_response + mock_polling_method.return_value = mock_polling_method + mock_polling_method._initial_response = mock_initial_response + + # _pipeline_response is not set yet (operation still in progress) + del mock_polling_method._pipeline_response + + poller = AnalyzeLROPoller( + client=Mock(), + initial_response=Mock(), + deserialization_callback=Mock(), + polling_method=mock_polling_method, + ) + + usage = poller.usage + assert usage is None diff --git a/sdk/contentunderstanding/azure-ai-contentunderstanding/tests/test_content_understanding_content_analyzers_operations.py b/sdk/contentunderstanding/azure-ai-contentunderstanding/tests/test_content_understanding_content_analyzers_operations.py index a7592b044843..242843293ab0 100644 --- a/sdk/contentunderstanding/azure-ai-contentunderstanding/tests/test_content_understanding_content_analyzers_operations.py +++ b/sdk/contentunderstanding/azure-ai-contentunderstanding/tests/test_content_understanding_content_analyzers_operations.py @@ -21,6 +21,7 @@ new_simple_content_analyzer_object, new_marketing_video_analyzer_object, assert_poller_properties, + assert_analyze_poller_usage, assert_simple_content_analyzer_result, save_analysis_result_to_file, save_keyframe_image_to_file, @@ -604,6 +605,7 @@ def test_content_analyzers_analyze_binary_extract_markdown(self, contentundersta # Wait for completion result = poller.result() assert_poller_properties(poller) + assert_analyze_poller_usage(poller) # Verify result assert result is not None, "Analysis result should not be null" @@ -739,6 +741,7 @@ def test_content_analyzers_analyze_configs(self, contentunderstanding_endpoint: # Wait for completion result = poller.result() assert_poller_properties(poller) + assert_analyze_poller_usage(poller) # Verify result assert result is not None, "Analysis result should not be null" @@ -801,6 +804,7 @@ def test_content_analyzers_analyze_return_raw_json(self, contentunderstanding_en # Wait for completion result = poller.result() assert_poller_properties(poller) + assert_analyze_poller_usage(poller) # Verify operation completed successfully assert result is not None, "Analysis result should not be null" diff --git a/sdk/contentunderstanding/azure-ai-contentunderstanding/tests/test_content_understanding_content_analyzers_operations_async.py b/sdk/contentunderstanding/azure-ai-contentunderstanding/tests/test_content_understanding_content_analyzers_operations_async.py index e2f42ae04a9c..d5b8450ed9ab 100644 --- a/sdk/contentunderstanding/azure-ai-contentunderstanding/tests/test_content_understanding_content_analyzers_operations_async.py +++ b/sdk/contentunderstanding/azure-ai-contentunderstanding/tests/test_content_understanding_content_analyzers_operations_async.py @@ -21,6 +21,7 @@ new_invoice_analyzer_object, new_marketing_video_analyzer_object, assert_poller_properties, + assert_analyze_poller_usage, assert_simple_content_analyzer_result, assert_invoice_fields, assert_document_properties, @@ -978,6 +979,7 @@ async def test_analyze_binary_extract_markdown_async(self, contentunderstanding_ # Wait for completion result = await poller.result() assert_poller_properties(poller) + assert_analyze_poller_usage(poller) # Verify result assert result is not None, "Analysis result should not be null" @@ -1113,6 +1115,7 @@ async def test_analyze_configs_async(self, contentunderstanding_endpoint: str) - # Wait for completion result = await poller.result() assert_poller_properties(poller) + assert_analyze_poller_usage(poller) # Verify result assert result is not None, "Analysis result should not be null" @@ -1175,6 +1178,7 @@ async def test_analyze_return_raw_json_async(self, contentunderstanding_endpoint # Wait for completion result = await poller.result() assert_poller_properties(poller) + assert_analyze_poller_usage(poller) # Verify operation completed successfully assert result is not None, "Analysis result should not be null" diff --git a/sdk/contentunderstanding/azure-ai-contentunderstanding/tests/test_helpers.py b/sdk/contentunderstanding/azure-ai-contentunderstanding/tests/test_helpers.py index 5d405339cabf..57e1d071c2c4 100644 --- a/sdk/contentunderstanding/azure-ai-contentunderstanding/tests/test_helpers.py +++ b/sdk/contentunderstanding/azure-ai-contentunderstanding/tests/test_helpers.py @@ -132,6 +132,25 @@ def assert_poller_properties(poller: Any, poller_name: str = "Poller") -> None: print(f"{poller_name} properties verified successfully") +def assert_analyze_poller_usage(poller: Any, poller_name: str = "Poller") -> None: + """Assert the usage property on a completed analyze poller (AnalyzeLROPoller / AnalyzeAsyncLROPoller). + + Must be called AFTER poller.result() has been called, since usage is only available + once the LRO has completed and the final polling response contains the usage data. + + Args: + poller: The AnalyzeLROPoller or AnalyzeAsyncLROPoller instance to validate + poller_name: Optional name for the poller in log messages + + Raises: + AssertionError: If the usage property is not available or invalid + """ + assert hasattr(poller, "usage"), f"{poller_name} should have a 'usage' property" + usage = poller.usage + assert usage is not None, f"{poller_name} usage should not be None after completion" + print(f"{poller_name} usage verified: {usage}") + + def assert_simple_content_analyzer_result(analysis_result: Any, result_name: str = "Analysis result") -> None: """Assert simple content analyzer result properties and field extraction. From 7fb940cb712033ef7cbfd1e65a75cd4c763b454e Mon Sep 17 00:00:00 2001 From: chienyuanchang Date: Mon, 13 Apr 2026 10:41:36 -0700 Subject: [PATCH 02/13] add usage into sample --- .../sample_analyze_invoice_async.py | 26 ++++++++++++++++++ .../samples/sample_analyze_invoice.py | 27 +++++++++++++++++++ .../samples/test_sample_analyze_invoice.py | 22 +++++++++++++++ .../test_sample_analyze_invoice_async.py | 22 +++++++++++++++ 4 files changed, 97 insertions(+) diff --git a/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/async_samples/sample_analyze_invoice_async.py b/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/async_samples/sample_analyze_invoice_async.py index 6cbea029dc7d..5b5a7c054ed4 100644 --- a/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/async_samples/sample_analyze_invoice_async.py +++ b/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/async_samples/sample_analyze_invoice_async.py @@ -211,6 +211,32 @@ async def main() -> None: ) # [END extract_invoice_fields] + # [START get_usage] + # Access usage details from the poller (available after result() completes). + # Usage provides billing and resource consumption information for the analysis, + # including document pages processed, contextualization tokens, and LLM/embedding + # tokens consumed grouped by model. + usage = poller.usage + if usage: + print("\nUsage Details:") + if usage.document_pages_standard: + print(f" Document pages (standard): {usage.document_pages_standard}") + if usage.document_pages_basic: + print(f" Document pages (basic): {usage.document_pages_basic}") + if usage.document_pages_minimal: + print(f" Document pages (minimal): {usage.document_pages_minimal}") + if usage.audio_hours: + print(f" Audio hours: {usage.audio_hours}") + if usage.video_hours: + print(f" Video hours: {usage.video_hours}") + if usage.contextualization_tokens: + print(f" Contextualization tokens: {usage.contextualization_tokens}") + if usage.tokens: + print(" Model tokens:") + for model, count in usage.tokens.items(): + print(f" {model}: {count}") + # [END get_usage] + if not isinstance(credential, AzureKeyCredential): await credential.close() diff --git a/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/sample_analyze_invoice.py b/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/sample_analyze_invoice.py index 38542a8a3fa8..6a8dc9c846b9 100644 --- a/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/sample_analyze_invoice.py +++ b/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/sample_analyze_invoice.py @@ -205,6 +205,33 @@ def main() -> None: ) # [END extract_invoice_fields] + # [START get_usage] + # Access usage details from the poller (available after result() completes). + # Usage provides billing and resource consumption information for the analysis, + # including document pages processed, contextualization tokens, and LLM/embedding + # tokens consumed grouped by model. + usage = poller.usage + if usage: + print("\nUsage Details:") + if usage.document_pages_standard: + print(f" Document pages (standard): {usage.document_pages_standard}") + if usage.document_pages_basic: + print(f" Document pages (basic): {usage.document_pages_basic}") + if usage.document_pages_minimal: + print(f" Document pages (minimal): {usage.document_pages_minimal}") + if usage.audio_hours: + print(f" Audio hours: {usage.audio_hours}") + if usage.video_hours: + print(f" Video hours: {usage.video_hours}") + if usage.contextualization_tokens: + print(f" Contextualization tokens: {usage.contextualization_tokens}") + if usage.tokens: + print(" Model tokens:") + for model, count in usage.tokens.items(): + print(f" {model}: {count}") + # [END get_usage] + if __name__ == "__main__": main() + diff --git a/sdk/contentunderstanding/azure-ai-contentunderstanding/tests/samples/test_sample_analyze_invoice.py b/sdk/contentunderstanding/azure-ai-contentunderstanding/tests/samples/test_sample_analyze_invoice.py index 865815bd13cf..97015d1e95cc 100644 --- a/sdk/contentunderstanding/azure-ai-contentunderstanding/tests/samples/test_sample_analyze_invoice.py +++ b/sdk/contentunderstanding/azure-ai-contentunderstanding/tests/samples/test_sample_analyze_invoice.py @@ -215,4 +215,26 @@ def test_sample_analyze_invoice(self, contentunderstanding_endpoint: str, **kwar else: print("[INFO] LineItems field not found in this document") + # Verify usage details (available after result() completes) + usage = poller.usage + assert usage is not None, "Usage details should be available after analysis" + print("[PASS] Usage details available") + + # Verify at least one page metric is set + has_pages = ( + (usage.document_pages_standard is not None and usage.document_pages_standard > 0) + or (usage.document_pages_basic is not None and usage.document_pages_basic > 0) + or (usage.document_pages_minimal is not None and usage.document_pages_minimal > 0) + ) + assert has_pages, "Usage should report at least one document page metric" + print(f"[PASS] Usage reports document pages (standard={usage.document_pages_standard}, basic={usage.document_pages_basic}, minimal={usage.document_pages_minimal})") + + # Verify token usage is reported + if usage.tokens: + assert len(usage.tokens) > 0, "Token usage should have at least one model entry" + for model, count in usage.tokens.items(): + assert count > 0, f"Token count for {model} should be positive" + print(f"[INFO] Token usage: {model} = {count}") + print(f"[PASS] Token usage reported for {len(usage.tokens)} model(s)") + print("\n[SUCCESS] All test_sample_analyze_invoice assertions passed") diff --git a/sdk/contentunderstanding/azure-ai-contentunderstanding/tests/samples/test_sample_analyze_invoice_async.py b/sdk/contentunderstanding/azure-ai-contentunderstanding/tests/samples/test_sample_analyze_invoice_async.py index 1015b54454a9..d041c091064a 100644 --- a/sdk/contentunderstanding/azure-ai-contentunderstanding/tests/samples/test_sample_analyze_invoice_async.py +++ b/sdk/contentunderstanding/azure-ai-contentunderstanding/tests/samples/test_sample_analyze_invoice_async.py @@ -214,5 +214,27 @@ async def test_sample_analyze_invoice_async(self, contentunderstanding_endpoint: else: print("[INFO] LineItems field not found in this document") + # Verify usage details (available after result() completes) + usage = poller.usage + assert usage is not None, "Usage details should be available after analysis" + print("[PASS] Usage details available") + + # Verify at least one page metric is set + has_pages = ( + (usage.document_pages_standard is not None and usage.document_pages_standard > 0) + or (usage.document_pages_basic is not None and usage.document_pages_basic > 0) + or (usage.document_pages_minimal is not None and usage.document_pages_minimal > 0) + ) + assert has_pages, "Usage should report at least one document page metric" + print(f"[PASS] Usage reports document pages (standard={usage.document_pages_standard}, basic={usage.document_pages_basic}, minimal={usage.document_pages_minimal})") + + # Verify token usage is reported + if usage.tokens: + assert len(usage.tokens) > 0, "Token usage should have at least one model entry" + for model, count in usage.tokens.items(): + assert count > 0, f"Token count for {model} should be positive" + print(f"[INFO] Token usage: {model} = {count}") + print(f"[PASS] Token usage reported for {len(usage.tokens)} model(s)") + await client.close() print("\n[SUCCESS] All test_sample_analyze_invoice_async assertions passed") From 6fdd96cc55b58031cf95faddbc1b3f1d1aee8281 Mon Sep 17 00:00:00 2001 From: chienyuanchang Date: Mon, 13 Apr 2026 10:58:06 -0700 Subject: [PATCH 03/13] update version for release --- .../azure-ai-contentunderstanding/CHANGELOG.md | 5 +++++ .../azure-ai-contentunderstanding/README.md | 1 + .../azure/ai/contentunderstanding/_version.py | 2 +- .../azure-ai-contentunderstanding/samples/README.md | 1 + 4 files changed, 8 insertions(+), 1 deletion(-) diff --git a/sdk/contentunderstanding/azure-ai-contentunderstanding/CHANGELOG.md b/sdk/contentunderstanding/azure-ai-contentunderstanding/CHANGELOG.md index 948c75550a64..867068af6a93 100644 --- a/sdk/contentunderstanding/azure-ai-contentunderstanding/CHANGELOG.md +++ b/sdk/contentunderstanding/azure-ai-contentunderstanding/CHANGELOG.md @@ -1,5 +1,10 @@ # Release History +## 1.0.2 (2026-04-17) + +### Bugs Fixed +- Exposed `usage` property on `AnalyzeLROPoller` and `AnalyzeAsyncLROPoller` to surface billing and token consumption details (`UsageDetails`) returned by the REST API. Previously the `usage` field in the LRO response envelope was discarded by the generated deserialization callback. + ## 1.0.1 (2026-03-06) ### Bugs Fixed diff --git a/sdk/contentunderstanding/azure-ai-contentunderstanding/README.md b/sdk/contentunderstanding/azure-ai-contentunderstanding/README.md index fd8d79ffbbaa..9cb82881a725 100644 --- a/sdk/contentunderstanding/azure-ai-contentunderstanding/README.md +++ b/sdk/contentunderstanding/azure-ai-contentunderstanding/README.md @@ -31,6 +31,7 @@ This table shows the relationship between SDK versions and supported API service | SDK version | Supported API service version | | ----------- | ----------------------------- | +| 1.0.2 | 2025-11-01 | | 1.0.1 | 2025-11-01 | | 1.0.0 | 2025-11-01 | diff --git a/sdk/contentunderstanding/azure-ai-contentunderstanding/azure/ai/contentunderstanding/_version.py b/sdk/contentunderstanding/azure-ai-contentunderstanding/azure/ai/contentunderstanding/_version.py index 83602e6274bc..8e44259d1a18 100644 --- a/sdk/contentunderstanding/azure-ai-contentunderstanding/azure/ai/contentunderstanding/_version.py +++ b/sdk/contentunderstanding/azure-ai-contentunderstanding/azure/ai/contentunderstanding/_version.py @@ -6,4 +6,4 @@ # Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- -VERSION = "1.0.1" +VERSION = "1.0.2" diff --git a/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/README.md b/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/README.md index d96bc5f0cfc2..70ee18191d3f 100644 --- a/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/README.md +++ b/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/README.md @@ -63,6 +63,7 @@ Extracts structured fields from invoices using `prebuilt-invoice` analyzer. Show - Extracting structured fields (customer name, totals, dates, line items) - Working with field confidence scores and source locations - Accessing object fields and array fields +- Accessing usage details (billing metrics, token consumption per model) - Financial document processing (invoices, receipts, credit cards, bank statements, checks) ### Sample 04: Create Analyzer From 7976f69248da3028e50f017922be636fdbc4a76b Mon Sep 17 00:00:00 2001 From: chienyuanchang Date: Mon, 13 Apr 2026 11:06:43 -0700 Subject: [PATCH 04/13] remove unused prints --- .../samples/async_samples/sample_analyze_invoice_async.py | 8 -------- .../samples/sample_analyze_invoice.py | 8 -------- 2 files changed, 16 deletions(-) diff --git a/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/async_samples/sample_analyze_invoice_async.py b/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/async_samples/sample_analyze_invoice_async.py index 5b5a7c054ed4..d3387b0965f6 100644 --- a/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/async_samples/sample_analyze_invoice_async.py +++ b/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/async_samples/sample_analyze_invoice_async.py @@ -221,14 +221,6 @@ async def main() -> None: print("\nUsage Details:") if usage.document_pages_standard: print(f" Document pages (standard): {usage.document_pages_standard}") - if usage.document_pages_basic: - print(f" Document pages (basic): {usage.document_pages_basic}") - if usage.document_pages_minimal: - print(f" Document pages (minimal): {usage.document_pages_minimal}") - if usage.audio_hours: - print(f" Audio hours: {usage.audio_hours}") - if usage.video_hours: - print(f" Video hours: {usage.video_hours}") if usage.contextualization_tokens: print(f" Contextualization tokens: {usage.contextualization_tokens}") if usage.tokens: diff --git a/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/sample_analyze_invoice.py b/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/sample_analyze_invoice.py index 6a8dc9c846b9..898bfdee9df3 100644 --- a/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/sample_analyze_invoice.py +++ b/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/sample_analyze_invoice.py @@ -215,14 +215,6 @@ def main() -> None: print("\nUsage Details:") if usage.document_pages_standard: print(f" Document pages (standard): {usage.document_pages_standard}") - if usage.document_pages_basic: - print(f" Document pages (basic): {usage.document_pages_basic}") - if usage.document_pages_minimal: - print(f" Document pages (minimal): {usage.document_pages_minimal}") - if usage.audio_hours: - print(f" Audio hours: {usage.audio_hours}") - if usage.video_hours: - print(f" Video hours: {usage.video_hours}") if usage.contextualization_tokens: print(f" Contextualization tokens: {usage.contextualization_tokens}") if usage.tokens: From ccd7269875ac2e8699b31e65fff10c54dceb3fe6 Mon Sep 17 00:00:00 2001 From: chienyuanchang Date: Mon, 13 Apr 2026 13:54:44 -0700 Subject: [PATCH 05/13] add more error type, use is not None, remove dead test code --- .../azure/ai/contentunderstanding/aio/models/_patch.py | 2 +- .../azure/ai/contentunderstanding/models/_patch.py | 2 +- .../samples/async_samples/sample_analyze_invoice_async.py | 4 ++-- .../samples/sample_analyze_invoice.py | 4 ++-- .../tests/test_analyzer_operation_id.py | 6 +++--- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/sdk/contentunderstanding/azure-ai-contentunderstanding/azure/ai/contentunderstanding/aio/models/_patch.py b/sdk/contentunderstanding/azure-ai-contentunderstanding/azure/ai/contentunderstanding/aio/models/_patch.py index aad46cd7e183..1e17f2e7e876 100644 --- a/sdk/contentunderstanding/azure-ai-contentunderstanding/azure/ai/contentunderstanding/aio/models/_patch.py +++ b/sdk/contentunderstanding/azure-ai-contentunderstanding/azure/ai/contentunderstanding/aio/models/_patch.py @@ -82,7 +82,7 @@ def usage(self) -> Optional["_models.UsageDetails"]: if usage_data is None: return None return _deserialize(_models.UsageDetails, usage_data) - except AttributeError: + except (AttributeError, TypeError, ValueError): return None @classmethod diff --git a/sdk/contentunderstanding/azure-ai-contentunderstanding/azure/ai/contentunderstanding/models/_patch.py b/sdk/contentunderstanding/azure-ai-contentunderstanding/azure/ai/contentunderstanding/models/_patch.py index 3cf79f823e54..04ea4b84806a 100644 --- a/sdk/contentunderstanding/azure-ai-contentunderstanding/azure/ai/contentunderstanding/models/_patch.py +++ b/sdk/contentunderstanding/azure-ai-contentunderstanding/azure/ai/contentunderstanding/models/_patch.py @@ -175,7 +175,7 @@ def usage(self) -> Optional["_models.UsageDetails"]: if usage_data is None: return None return _deserialize(_models.UsageDetails, usage_data) - except AttributeError: + except (AttributeError, TypeError, ValueError): return None @classmethod diff --git a/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/async_samples/sample_analyze_invoice_async.py b/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/async_samples/sample_analyze_invoice_async.py index d3387b0965f6..cda40ef15d00 100644 --- a/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/async_samples/sample_analyze_invoice_async.py +++ b/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/async_samples/sample_analyze_invoice_async.py @@ -219,9 +219,9 @@ async def main() -> None: usage = poller.usage if usage: print("\nUsage Details:") - if usage.document_pages_standard: + if usage.document_pages_standard is not None: print(f" Document pages (standard): {usage.document_pages_standard}") - if usage.contextualization_tokens: + if usage.contextualization_tokens is not None: print(f" Contextualization tokens: {usage.contextualization_tokens}") if usage.tokens: print(" Model tokens:") diff --git a/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/sample_analyze_invoice.py b/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/sample_analyze_invoice.py index 898bfdee9df3..745669a1568a 100644 --- a/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/sample_analyze_invoice.py +++ b/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/sample_analyze_invoice.py @@ -213,9 +213,9 @@ def main() -> None: usage = poller.usage if usage: print("\nUsage Details:") - if usage.document_pages_standard: + if usage.document_pages_standard is not None: print(f" Document pages (standard): {usage.document_pages_standard}") - if usage.contextualization_tokens: + if usage.contextualization_tokens is not None: print(f" Contextualization tokens: {usage.contextualization_tokens}") if usage.tokens: print(" Model tokens:") diff --git a/sdk/contentunderstanding/azure-ai-contentunderstanding/tests/test_analyzer_operation_id.py b/sdk/contentunderstanding/azure-ai-contentunderstanding/tests/test_analyzer_operation_id.py index 6fc3f1ee1fdb..312ed536fc0c 100644 --- a/sdk/contentunderstanding/azure-ai-contentunderstanding/tests/test_analyzer_operation_id.py +++ b/sdk/contentunderstanding/azure-ai-contentunderstanding/tests/test_analyzer_operation_id.py @@ -265,11 +265,11 @@ def test_usage_returns_none_before_completion(self): "Operation-Location": "https://endpoint/contentunderstanding/analyzerResults/test-op-id?api-version=2025-11-01" } mock_initial_response.http_response = mock_http_response - mock_polling_method.return_value = mock_polling_method mock_polling_method._initial_response = mock_initial_response - # _pipeline_response is not set yet (operation still in progress) - del mock_polling_method._pipeline_response + # Set _pipeline_response to None to simulate an operation still in progress + # (before result() completes). Accessing None.http_response raises AttributeError. + mock_polling_method._pipeline_response = None poller = AnalyzeLROPoller( client=Mock(), From 396f7e0109e546dffe120e16cf4cdefd2315e773 Mon Sep 17 00:00:00 2001 From: chienyuanchang Date: Mon, 13 Apr 2026 13:58:20 -0700 Subject: [PATCH 06/13] remove trailing line --- .../samples/sample_analyze_invoice.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/sample_analyze_invoice.py b/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/sample_analyze_invoice.py index 745669a1568a..52053ec952f4 100644 --- a/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/sample_analyze_invoice.py +++ b/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/sample_analyze_invoice.py @@ -226,4 +226,3 @@ def main() -> None: if __name__ == "__main__": main() - From 50250f35d239150a2611267353d7ee5b45a8eb0a Mon Sep 17 00:00:00 2001 From: chienyuanchang Date: Tue, 14 Apr 2026 11:17:21 -0700 Subject: [PATCH 07/13] info for token, handles the only legitimate None case for getting usage --- .../contentunderstanding/aio/models/_patch.py | 13 ++++++------- .../ai/contentunderstanding/models/_patch.py | 13 ++++++------- .../sample_analyze_invoice_async.py | 19 ++++++++++++++++--- .../samples/sample_analyze_invoice.py | 19 ++++++++++++++++--- 4 files changed, 44 insertions(+), 20 deletions(-) diff --git a/sdk/contentunderstanding/azure-ai-contentunderstanding/azure/ai/contentunderstanding/aio/models/_patch.py b/sdk/contentunderstanding/azure-ai-contentunderstanding/azure/ai/contentunderstanding/aio/models/_patch.py index 1e17f2e7e876..df2ac0fdbe6b 100644 --- a/sdk/contentunderstanding/azure-ai-contentunderstanding/azure/ai/contentunderstanding/aio/models/_patch.py +++ b/sdk/contentunderstanding/azure-ai-contentunderstanding/azure/ai/contentunderstanding/aio/models/_patch.py @@ -76,14 +76,13 @@ def usage(self) -> Optional["_models.UsageDetails"]: or usage information is not available. :rtype: ~azure.ai.contentunderstanding.models.UsageDetails or None """ - try: - response = self.polling_method()._pipeline_response.http_response # type: ignore # pylint: disable=protected-access - usage_data = response.json().get("usage") - if usage_data is None: - return None - return _deserialize(_models.UsageDetails, usage_data) - except (AttributeError, TypeError, ValueError): + if not self.done(): + return None + response = self.polling_method()._pipeline_response.http_response # type: ignore # pylint: disable=protected-access + usage_data = response.json().get("usage") + if usage_data is None: return None + return _deserialize(_models.UsageDetails, usage_data) @classmethod def from_poller(cls, poller: AsyncLROPoller[PollingReturnType_co]) -> "AnalyzeAsyncLROPoller[PollingReturnType_co]": # pyright: ignore[reportInvalidTypeArguments] # fmt: skip diff --git a/sdk/contentunderstanding/azure-ai-contentunderstanding/azure/ai/contentunderstanding/models/_patch.py b/sdk/contentunderstanding/azure-ai-contentunderstanding/azure/ai/contentunderstanding/models/_patch.py index 04ea4b84806a..199ca653e0c7 100644 --- a/sdk/contentunderstanding/azure-ai-contentunderstanding/azure/ai/contentunderstanding/models/_patch.py +++ b/sdk/contentunderstanding/azure-ai-contentunderstanding/azure/ai/contentunderstanding/models/_patch.py @@ -169,14 +169,13 @@ def usage(self) -> Optional["_models.UsageDetails"]: or usage information is not available. :rtype: ~azure.ai.contentunderstanding.models.UsageDetails or None """ - try: - response = self.polling_method()._pipeline_response.http_response # type: ignore # pylint: disable=protected-access - usage_data = response.json().get("usage") - if usage_data is None: - return None - return _deserialize(_models.UsageDetails, usage_data) - except (AttributeError, TypeError, ValueError): + if not self.done(): + return None + response = self.polling_method()._pipeline_response.http_response # type: ignore # pylint: disable=protected-access + usage_data = response.json().get("usage") + if usage_data is None: return None + return _deserialize(_models.UsageDetails, usage_data) @classmethod def from_poller(cls, poller: LROPoller[PollingReturnType_co]) -> "AnalyzeLROPoller[PollingReturnType_co]": # pyright: ignore[reportInvalidTypeArguments] # fmt: skip diff --git a/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/async_samples/sample_analyze_invoice_async.py b/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/async_samples/sample_analyze_invoice_async.py index cda40ef15d00..62da86d9955c 100644 --- a/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/async_samples/sample_analyze_invoice_async.py +++ b/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/async_samples/sample_analyze_invoice_async.py @@ -213,9 +213,22 @@ async def main() -> None: # [START get_usage] # Access usage details from the poller (available after result() completes). - # Usage provides billing and resource consumption information for the analysis, - # including document pages processed, contextualization tokens, and LLM/embedding - # tokens consumed grouped by model. + # Usage reports resource consumption for billing estimation: + # + # - document_pages_standard/basic/minimal: Pages processed at each extraction tier. + # Standard = layout + OCR (scanned docs), Basic = OCR only, Minimal = digital formats + # (DOCX, XLSX, HTML, TXT) that need no OCR. Charged per 1,000 pages. + # + # - contextualization_tokens: Fixed-rate tokens charged by Content Understanding for + # preparing context, generating confidence scores, source grounding, and formatting + # output. Typically 1,000 tokens per page. Charged separately from LLM tokens. + # + # - tokens: Dict of "{model}-input" / "{model}-output" token counts consumed by your + # Foundry model deployment (e.g. "gpt-4.1-input", "gpt-4.1-output"). These are + # billed on your Foundry deployment, not on Content Understanding. + # + # For full pricing details, see: + # https://learn.microsoft.com/azure/ai-services/content-understanding/pricing-explainer usage = poller.usage if usage: print("\nUsage Details:") diff --git a/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/sample_analyze_invoice.py b/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/sample_analyze_invoice.py index 52053ec952f4..c7812c180597 100644 --- a/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/sample_analyze_invoice.py +++ b/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/sample_analyze_invoice.py @@ -207,9 +207,22 @@ def main() -> None: # [START get_usage] # Access usage details from the poller (available after result() completes). - # Usage provides billing and resource consumption information for the analysis, - # including document pages processed, contextualization tokens, and LLM/embedding - # tokens consumed grouped by model. + # Usage reports resource consumption for billing estimation: + # + # - document_pages_standard/basic/minimal: Pages processed at each extraction tier. + # Standard = layout + OCR (scanned docs), Basic = OCR only, Minimal = digital formats + # (DOCX, XLSX, HTML, TXT) that need no OCR. Charged per 1,000 pages. + # + # - contextualization_tokens: Fixed-rate tokens charged by Content Understanding for + # preparing context, generating confidence scores, source grounding, and formatting + # output. Typically 1,000 tokens per page. Charged separately from LLM tokens. + # + # - tokens: Dict of "{model}-input" / "{model}-output" token counts consumed by your + # Foundry model deployment (e.g. "gpt-4.1-input", "gpt-4.1-output"). These are + # billed on your Foundry deployment, not on Content Understanding. + # + # For full pricing details, see: + # https://learn.microsoft.com/azure/ai-services/content-understanding/pricing-explainer usage = poller.usage if usage: print("\nUsage Details:") From 54f5c2b0a51a970a5815f527a1f888683e748132 Mon Sep 17 00:00:00 2001 From: chienyuanchang Date: Tue, 14 Apr 2026 11:19:43 -0700 Subject: [PATCH 08/13] fix incorrect note in sample_grant_copy_auth --- .../samples/async_samples/sample_grant_copy_auth_async.py | 4 ++-- .../samples/sample_grant_copy_auth.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/async_samples/sample_grant_copy_auth_async.py b/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/async_samples/sample_grant_copy_auth_async.py index 59e83da8c893..5d6355bdd5c4 100644 --- a/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/async_samples/sample_grant_copy_auth_async.py +++ b/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/async_samples/sample_grant_copy_auth_async.py @@ -75,8 +75,8 @@ Example resource ID format: /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.CognitiveServices/accounts/{name} - Important: Cross-resource copying requires credential-based authentication (such as DefaultAzureCredential). - API keys cannot be used for cross-resource operations. + Note: If API keys are not provided, DefaultAzureCredential will be used. + Cross-resource copying with DefaultAzureCredential requires 'Cognitive Services User' role on both source and target resources. """ import asyncio diff --git a/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/sample_grant_copy_auth.py b/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/sample_grant_copy_auth.py index 17cd03af239c..fb618b29c497 100644 --- a/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/sample_grant_copy_auth.py +++ b/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/sample_grant_copy_auth.py @@ -75,8 +75,8 @@ Example resource ID format: /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.CognitiveServices/accounts/{name} - Important: Cross-resource copying requires credential-based authentication (such as DefaultAzureCredential). - API keys cannot be used for cross-resource operations. + Note: If API keys are not provided, DefaultAzureCredential will be used. + Cross-resource copying with DefaultAzureCredential requires 'Cognitive Services User' role on both source and target resources. """ import os From f092008c3e6b115c172dae0f8e6d2056eddaf8e7 Mon Sep 17 00:00:00 2001 From: chienyuanchang Date: Tue, 14 Apr 2026 11:22:44 -0700 Subject: [PATCH 09/13] update release date to 2026-04-15 --- .../azure-ai-contentunderstanding/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/contentunderstanding/azure-ai-contentunderstanding/CHANGELOG.md b/sdk/contentunderstanding/azure-ai-contentunderstanding/CHANGELOG.md index 867068af6a93..5c68748e3334 100644 --- a/sdk/contentunderstanding/azure-ai-contentunderstanding/CHANGELOG.md +++ b/sdk/contentunderstanding/azure-ai-contentunderstanding/CHANGELOG.md @@ -1,6 +1,6 @@ # Release History -## 1.0.2 (2026-04-17) +## 1.0.2 (2026-04-15) ### Bugs Fixed - Exposed `usage` property on `AnalyzeLROPoller` and `AnalyzeAsyncLROPoller` to surface billing and token consumption details (`UsageDetails`) returned by the REST API. Previously the `usage` field in the LRO response envelope was discarded by the generated deserialization callback. From 611735f54331132b704ae261ca11bd9de6e280ba Mon Sep 17 00:00:00 2001 From: chienyuanchang Date: Tue, 14 Apr 2026 15:51:10 -0700 Subject: [PATCH 10/13] Updtae test, add async test --- .../tests/test_analyzer_operation_id.py | 18 ++-- .../tests/test_analyzer_operation_id_async.py | 90 +++++++++++++++++++ 2 files changed, 97 insertions(+), 11 deletions(-) create mode 100644 sdk/contentunderstanding/azure-ai-contentunderstanding/tests/test_analyzer_operation_id_async.py diff --git a/sdk/contentunderstanding/azure-ai-contentunderstanding/tests/test_analyzer_operation_id.py b/sdk/contentunderstanding/azure-ai-contentunderstanding/tests/test_analyzer_operation_id.py index 312ed536fc0c..c1e0a377b9d1 100644 --- a/sdk/contentunderstanding/azure-ai-contentunderstanding/tests/test_analyzer_operation_id.py +++ b/sdk/contentunderstanding/azure-ai-contentunderstanding/tests/test_analyzer_operation_id.py @@ -259,17 +259,6 @@ def test_usage_returns_none_when_not_present(self): def test_usage_returns_none_before_completion(self): """Test usage property returns None when the polling has not yet completed.""" mock_polling_method = Mock() - mock_initial_response = Mock() - mock_http_response = Mock() - mock_http_response.headers = { - "Operation-Location": "https://endpoint/contentunderstanding/analyzerResults/test-op-id?api-version=2025-11-01" - } - mock_initial_response.http_response = mock_http_response - mock_polling_method._initial_response = mock_initial_response - - # Set _pipeline_response to None to simulate an operation still in progress - # (before result() completes). Accessing None.http_response raises AttributeError. - mock_polling_method._pipeline_response = None poller = AnalyzeLROPoller( client=Mock(), @@ -278,5 +267,12 @@ def test_usage_returns_none_before_completion(self): polling_method=mock_polling_method, ) + # Simulate an in-progress operation: LROPoller.done() returns True when + # _thread is None, so set _thread to a mock with is_alive() returning True. + mock_thread = Mock() + mock_thread.is_alive.return_value = True + poller._thread = mock_thread + + assert not poller.done() usage = poller.usage assert usage is None diff --git a/sdk/contentunderstanding/azure-ai-contentunderstanding/tests/test_analyzer_operation_id_async.py b/sdk/contentunderstanding/azure-ai-contentunderstanding/tests/test_analyzer_operation_id_async.py new file mode 100644 index 000000000000..7766b1c4c0cc --- /dev/null +++ b/sdk/contentunderstanding/azure-ai-contentunderstanding/tests/test_analyzer_operation_id_async.py @@ -0,0 +1,90 @@ +# pylint: disable=line-too-long,useless-suppression +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------- + +""" +Tests for Content Understanding async analyzer operation ID and usage functionality. +""" + +from unittest.mock import Mock +from azure.ai.contentunderstanding.aio.models._patch import AnalyzeAsyncLROPoller +from azure.ai.contentunderstanding.models import UsageDetails + + +class TestAnalyzeAsyncLROPollerUsage: + """Test the usage property on AnalyzeAsyncLROPoller (async counterpart).""" + + def _make_async_poller_with_usage(self, usage_json, done=True): + """Helper to create an AnalyzeAsyncLROPoller with a mocked final response.""" + mock_polling_method = Mock() + mock_initial_response = Mock() + mock_http_response = Mock() + mock_http_response.headers = { + "Operation-Location": "https://endpoint/contentunderstanding/analyzerResults/test-op-id?api-version=2025-11-01" + } + mock_initial_response.http_response = mock_http_response + mock_polling_method.return_value = mock_polling_method + mock_polling_method._initial_response = mock_initial_response + + mock_final_response = Mock() + mock_final_http_response = Mock() + response_json = { + "id": "test-op-id", + "status": "Succeeded", + "result": {"contents": []}, + } + if usage_json is not None: + response_json["usage"] = usage_json + mock_final_http_response.json.return_value = response_json + mock_final_response.http_response = mock_final_http_response + mock_polling_method._pipeline_response = mock_final_response + + poller = AnalyzeAsyncLROPoller( + client=Mock(), + initial_response=Mock(), + deserialization_callback=Mock(), + polling_method=mock_polling_method, + ) + # AsyncLROPoller.done() checks self._done (defaults to False after __init__) + poller._done = done + return poller + + def test_async_usage_with_full_data(self): + """Test async usage property returns UsageDetails with all fields populated.""" + usage_json = { + "documentPagesMinimal": 0, + "documentPagesBasic": 0, + "documentPagesStandard": 2, + "audioHours": 0.0, + "videoHours": 0.0, + "contextualizationTokens": 1500, + "tokens": { + "gpt-4.1-input": 500, + "gpt-4.1-output": 200, + }, + } + + poller = self._make_async_poller_with_usage(usage_json) + usage = poller.usage + + assert usage is not None + assert isinstance(usage, UsageDetails) + assert usage.document_pages_standard == 2 + assert usage.contextualization_tokens == 1500 + assert usage.tokens == {"gpt-4.1-input": 500, "gpt-4.1-output": 200} + + def test_async_usage_returns_none_when_not_present(self): + """Test async usage property returns None when usage is not in the response.""" + poller = self._make_async_poller_with_usage(None) + usage = poller.usage + assert usage is None + + def test_async_usage_returns_none_before_completion(self): + """Test async usage property returns None when the polling has not yet completed.""" + poller = self._make_async_poller_with_usage({"documentPagesStandard": 1}, done=False) + assert not poller.done() + usage = poller.usage + assert usage is None From ec9873e94eccdafa633620854c8fc1801d3a40ed Mon Sep 17 00:00:00 2001 From: chienyuanchang Date: Tue, 14 Apr 2026 15:59:16 -0700 Subject: [PATCH 11/13] remove unnecessary f-strings --- .../sample_analyze_binary_async.py | 4 +-- .../async_samples/sample_analyze_url_async.py | 28 +++++++++---------- .../sample_copy_analyzer_async.py | 6 ++-- .../sample_grant_copy_auth_async.py | 16 +++++------ .../samples/sample_analyze_url.py | 28 +++++++++---------- .../samples/sample_copy_analyzer.py | 6 ++-- .../samples/sample_grant_copy_auth.py | 16 +++++------ 7 files changed, 52 insertions(+), 52 deletions(-) diff --git a/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/async_samples/sample_analyze_binary_async.py b/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/async_samples/sample_analyze_binary_async.py index 693c28b719d4..f3c9c9c62403 100644 --- a/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/async_samples/sample_analyze_binary_async.py +++ b/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/async_samples/sample_analyze_binary_async.py @@ -107,7 +107,7 @@ async def main() -> None: if isinstance(range_result.contents[0], DocumentContent): range_doc = range_result.contents[0] print( - f"Content range analysis returned pages" + "Content range analysis returned pages" f" {range_doc.start_page_number} - {range_doc.end_page_number}" ) # [END analyze_binary_with_content_range] @@ -125,7 +125,7 @@ async def main() -> None: if isinstance(combine_range_result.contents[0], DocumentContent): combine_doc = combine_range_result.contents[0] print( - f"Combined content range analysis returned pages" + "Combined content range analysis returned pages" f" {combine_doc.start_page_number} - {combine_doc.end_page_number}" ) # [END analyze_binary_with_combined_content_range] diff --git a/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/async_samples/sample_analyze_url_async.py b/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/async_samples/sample_analyze_url_async.py index 04562c733718..e4cca23f5474 100644 --- a/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/async_samples/sample_analyze_url_async.py +++ b/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/async_samples/sample_analyze_url_async.py @@ -74,7 +74,7 @@ async def main() -> None: # You can replace this URL with your own publicly accessible document URL. document_url = "https://raw.githubusercontent.com/Azure-Samples/azure-ai-content-understanding-assets/main/document/mixed_financial_docs.pdf" - print(f"Analyzing document from URL with prebuilt-documentSearch...") + print("Analyzing document from URL with prebuilt-documentSearch...") print(f" URL: {document_url}") poller = await client.begin_analyze( @@ -119,7 +119,7 @@ async def main() -> None: range_doc_content = cast(DocumentContent, range_result.contents[0]) print( - f"Content range analysis returned pages" + "Content range analysis returned pages" f" {range_doc_content.start_page_number} - {range_doc_content.end_page_number}" ) @@ -133,7 +133,7 @@ async def main() -> None: combine_doc_content = cast(DocumentContent, combine_range_result.contents[0]) print( - f"Combined content range analysis returned pages" + "Combined content range analysis returned pages" f" {combine_doc_content.start_page_number} - {combine_doc_content.end_page_number}" ) # [END analyze_document_url_with_content_range] @@ -144,7 +144,7 @@ async def main() -> None: print("=" * 60) video_url = "https://raw.githubusercontent.com/Azure-Samples/azure-ai-content-understanding-assets/main/videos/sdk_samples/FlightSimulator.mp4" - print(f"Analyzing video from URL with prebuilt-videoSearch...") + print("Analyzing video from URL with prebuilt-videoSearch...") print(f" URL: {video_url}") poller = await client.begin_analyze( @@ -197,7 +197,7 @@ async def main() -> None: for range_media in video_range_result.contents: range_video_content = cast(AudioVisualContent, range_media) print( - f"Content range segment:" + "Content range segment:" f" {range_video_content.start_time_ms} ms - {range_video_content.end_time_ms} ms" ) # [END analyze_video_url_with_content_range] @@ -220,7 +220,7 @@ async def main() -> None: for from_media in video_from_result.contents: from_video = cast(AudioVisualContent, from_media) print( - f"'10000-' segment:" + "'10000-' segment:" f" {from_video.start_time_ms} ms - {from_video.end_time_ms} ms" ) @@ -239,7 +239,7 @@ async def main() -> None: for subsec_media in video_subsec_result.contents: subsec_video = cast(AudioVisualContent, subsec_media) print( - f"'1200-3651' segment:" + "'1200-3651' segment:" f" {subsec_video.start_time_ms} ms - {subsec_video.end_time_ms} ms" ) @@ -258,7 +258,7 @@ async def main() -> None: for combine_media in video_combine_result.contents: combine_video = cast(AudioVisualContent, combine_media) print( - f"'0-3000,30000-' segment:" + "'0-3000,30000-' segment:" f" {combine_video.start_time_ms} ms - {combine_video.end_time_ms} ms" ) # [END analyze_video_url_with_additional_content_ranges] @@ -269,7 +269,7 @@ async def main() -> None: print("=" * 60) audio_url = "https://raw.githubusercontent.com/Azure-Samples/azure-ai-content-understanding-assets/main/audio/callCenterRecording.mp3" - print(f"Analyzing audio from URL with prebuilt-audioSearch...") + print("Analyzing audio from URL with prebuilt-audioSearch...") print(f" URL: {audio_url}") poller = await client.begin_analyze( @@ -316,7 +316,7 @@ async def main() -> None: range_audio_content = cast(AudioVisualContent, audio_range_result.contents[0]) print( - f"Content range audio segment:" + "Content range audio segment:" f" {range_audio_content.start_time_ms} ms - {range_audio_content.end_time_ms} ms" ) # [END analyze_audio_url_with_content_range] @@ -338,7 +338,7 @@ async def main() -> None: audio_from_result = await audio_from_poller.result() audio_from_content = cast(AudioVisualContent, audio_from_result.contents[0]) print( - f"'10000-':" + "'10000-':" f" {audio_from_content.start_time_ms} ms - {audio_from_content.end_time_ms} ms" ) @@ -356,7 +356,7 @@ async def main() -> None: audio_subsec_result = await audio_subsec_poller.result() audio_subsec_content = cast(AudioVisualContent, audio_subsec_result.contents[0]) print( - f"'1200-3651':" + "'1200-3651':" f" {audio_subsec_content.start_time_ms} ms - {audio_subsec_content.end_time_ms} ms" ) @@ -374,7 +374,7 @@ async def main() -> None: audio_combine_result = await audio_combine_poller.result() audio_combine_content = cast(AudioVisualContent, audio_combine_result.contents[0]) print( - f"'0-3000,30000-':" + "'0-3000,30000-':" f" {audio_combine_content.start_time_ms} ms - {audio_combine_content.end_time_ms} ms" ) # [END analyze_audio_url_with_additional_content_ranges] @@ -385,7 +385,7 @@ async def main() -> None: print("=" * 60) image_url = "https://raw.githubusercontent.com/Azure-Samples/azure-ai-content-understanding-assets/main/image/pieChart.jpg" - print(f"Analyzing image from URL with prebuilt-imageSearch...") + print("Analyzing image from URL with prebuilt-imageSearch...") print(f" URL: {image_url}") poller = await client.begin_analyze( diff --git a/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/async_samples/sample_copy_analyzer_async.py b/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/async_samples/sample_copy_analyzer_async.py index 3878d7e67eff..80a30a65b565 100644 --- a/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/async_samples/sample_copy_analyzer_async.py +++ b/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/async_samples/sample_copy_analyzer_async.py @@ -119,7 +119,7 @@ async def main() -> None: ) await poller.result() - print(f"Analyzer copied successfully!") + print("Analyzer copied successfully!") # [END copy_analyzer] # [START update_and_verify_analyzer] @@ -133,7 +133,7 @@ async def main() -> None: tags={"modelType": "model_in_production"}, ) - print(f"Updating target analyzer with production tag...") + print("Updating target analyzer with production tag...") await client.update_analyzer( analyzer_id=target_analyzer_id, resource=updated_analyzer ) @@ -148,7 +148,7 @@ async def main() -> None: # [END update_and_verify_analyzer] # [START delete_copied_analyzers] - print(f"\nCleaning up analyzers...") + print("\nCleaning up analyzers...") try: await client.delete_analyzer(analyzer_id=source_analyzer_id) diff --git a/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/async_samples/sample_grant_copy_auth_async.py b/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/async_samples/sample_grant_copy_auth_async.py index 5d6355bdd5c4..d9f2fa896490 100644 --- a/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/async_samples/sample_grant_copy_auth_async.py +++ b/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/async_samples/sample_grant_copy_auth_async.py @@ -208,7 +208,7 @@ async def main() -> None: resource=source_analyzer, ) await poller.result() - print(f" Source analyzer created successfully!") + print(" Source analyzer created successfully!") # Step 2: Grant copy authorization # Authorization must be granted by the source resource before the target resource can copy @@ -216,7 +216,7 @@ async def main() -> None: # - The source analyzer ID to be copied # - The target Azure resource ID that is allowed to receive the copy # - The target region where the copy will be performed (optional, defaults to current region) - print(f"\nStep 2: Granting copy authorization from source resource...") + print("\nStep 2: Granting copy authorization from source resource...") print(f" Target Azure Resource ID: {target_resource_id}") print(f" Target Region: {target_region}") @@ -226,7 +226,7 @@ async def main() -> None: target_region=target_region, ) - print(f" Authorization granted successfully!") + print(" Authorization granted successfully!") print(f" Target Azure Resource ID: {copy_auth.target_azure_resource_id}") print(f" Target Region: {target_region}") print(f" Expires at: {copy_auth.expires_at}") @@ -235,7 +235,7 @@ async def main() -> None: # The copy_analyzer method must be called on the target client because the target # resource is the one receiving and creating the copy. The target resource validates # that authorization was previously granted by the source resource. - print(f"\nStep 3: Copying analyzer from source to target...") + print("\nStep 3: Copying analyzer from source to target...") print(f" Source Analyzer ID: {source_analyzer_id}") print(f" Source Azure Resource ID: {source_resource_id}") print(f" Source Region: {source_region}") @@ -248,22 +248,22 @@ async def main() -> None: source_region=source_region, ) await copy_poller.result() - print(f" Analyzer copied successfully to target resource!") + print(" Analyzer copied successfully to target resource!") # Step 4: Verify the copy # Retrieve the analyzer from the target resource to verify the copy was successful - print(f"\nStep 4: Verifying the copied analyzer...") + print("\nStep 4: Verifying the copied analyzer...") copied_analyzer = await target_client.get_analyzer( analyzer_id=target_analyzer_id ) print(f" Target Analyzer ID: {copied_analyzer.analyzer_id}") print(f" Description: {copied_analyzer.description}") print(f" Status: {copied_analyzer.status}") - print(f"\nCross-resource copy completed successfully!") + print("\nCross-resource copy completed successfully!") finally: # Clean up - create new client instances for cleanup since the original ones are closed - print(f"\nCleaning up...") + print("\nCleaning up...") cleanup_source_client = ContentUnderstandingClient( endpoint=source_endpoint, credential=source_credential ) diff --git a/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/sample_analyze_url.py b/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/sample_analyze_url.py index 1be6e13c0073..0c3e9570d098 100644 --- a/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/sample_analyze_url.py +++ b/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/sample_analyze_url.py @@ -72,7 +72,7 @@ def main() -> None: # You can replace this URL with your own publicly accessible document URL. document_url = "https://raw.githubusercontent.com/Azure-Samples/azure-ai-content-understanding-assets/main/document/mixed_financial_docs.pdf" - print(f"Analyzing document from URL with prebuilt-documentSearch...") + print("Analyzing document from URL with prebuilt-documentSearch...") print(f" URL: {document_url}") poller = client.begin_analyze( @@ -117,7 +117,7 @@ def main() -> None: range_doc_content = cast(DocumentContent, range_result.contents[0]) print( - f"Content range analysis returned pages" + "Content range analysis returned pages" f" {range_doc_content.start_page_number} - {range_doc_content.end_page_number}" ) @@ -131,7 +131,7 @@ def main() -> None: combine_doc_content = cast(DocumentContent, combine_range_result.contents[0]) print( - f"Combined content range analysis returned pages" + "Combined content range analysis returned pages" f" {combine_doc_content.start_page_number} - {combine_doc_content.end_page_number}" ) # [END analyze_document_url_with_content_range] @@ -142,7 +142,7 @@ def main() -> None: print("=" * 60) video_url = "https://raw.githubusercontent.com/Azure-Samples/azure-ai-content-understanding-assets/main/videos/sdk_samples/FlightSimulator.mp4" - print(f"Analyzing video from URL with prebuilt-videoSearch...") + print("Analyzing video from URL with prebuilt-videoSearch...") print(f" URL: {video_url}") poller = client.begin_analyze( @@ -193,7 +193,7 @@ def main() -> None: for range_media in video_range_result.contents: range_video_content = cast(AudioVisualContent, range_media) print( - f"Content range segment:" + "Content range segment:" f" {range_video_content.start_time_ms} ms - {range_video_content.end_time_ms} ms" ) # [END analyze_video_url_with_content_range] @@ -216,7 +216,7 @@ def main() -> None: for from_media in video_from_result.contents: from_video = cast(AudioVisualContent, from_media) print( - f"'10000-' segment:" + "'10000-' segment:" f" {from_video.start_time_ms} ms - {from_video.end_time_ms} ms" ) @@ -235,7 +235,7 @@ def main() -> None: for subsec_media in video_subsec_result.contents: subsec_video = cast(AudioVisualContent, subsec_media) print( - f"'1200-3651' segment:" + "'1200-3651' segment:" f" {subsec_video.start_time_ms} ms - {subsec_video.end_time_ms} ms" ) @@ -254,7 +254,7 @@ def main() -> None: for combine_media in video_combine_result.contents: combine_video = cast(AudioVisualContent, combine_media) print( - f"'0-3000,30000-' segment:" + "'0-3000,30000-' segment:" f" {combine_video.start_time_ms} ms - {combine_video.end_time_ms} ms" ) # [END analyze_video_url_with_additional_content_ranges] @@ -265,7 +265,7 @@ def main() -> None: print("=" * 60) audio_url = "https://raw.githubusercontent.com/Azure-Samples/azure-ai-content-understanding-assets/main/audio/callCenterRecording.mp3" - print(f"Analyzing audio from URL with prebuilt-audioSearch...") + print("Analyzing audio from URL with prebuilt-audioSearch...") print(f" URL: {audio_url}") poller = client.begin_analyze( @@ -309,7 +309,7 @@ def main() -> None: range_audio_content = cast(AudioVisualContent, audio_range_result.contents[0]) print( - f"Content range audio segment:" + "Content range audio segment:" f" {range_audio_content.start_time_ms} ms - {range_audio_content.end_time_ms} ms" ) # [END analyze_audio_url_with_content_range] @@ -331,7 +331,7 @@ def main() -> None: audio_from_result = audio_from_poller.result() audio_from_content = cast(AudioVisualContent, audio_from_result.contents[0]) print( - f"'10000-':" + "'10000-':" f" {audio_from_content.start_time_ms} ms - {audio_from_content.end_time_ms} ms" ) @@ -349,7 +349,7 @@ def main() -> None: audio_subsec_result = audio_subsec_poller.result() audio_subsec_content = cast(AudioVisualContent, audio_subsec_result.contents[0]) print( - f"'1200-3651':" + "'1200-3651':" f" {audio_subsec_content.start_time_ms} ms - {audio_subsec_content.end_time_ms} ms" ) @@ -367,7 +367,7 @@ def main() -> None: audio_combine_result = audio_combine_poller.result() audio_combine_content = cast(AudioVisualContent, audio_combine_result.contents[0]) print( - f"'0-3000,30000-':" + "'0-3000,30000-':" f" {audio_combine_content.start_time_ms} ms - {audio_combine_content.end_time_ms} ms" ) # [END analyze_audio_url_with_additional_content_ranges] @@ -378,7 +378,7 @@ def main() -> None: print("=" * 60) image_url = "https://raw.githubusercontent.com/Azure-Samples/azure-ai-content-understanding-assets/main/image/pieChart.jpg" - print(f"Analyzing image from URL with prebuilt-imageSearch...") + print("Analyzing image from URL with prebuilt-imageSearch...") print(f" URL: {image_url}") poller = client.begin_analyze( diff --git a/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/sample_copy_analyzer.py b/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/sample_copy_analyzer.py index ceae81e28485..c023cb6c874f 100644 --- a/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/sample_copy_analyzer.py +++ b/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/sample_copy_analyzer.py @@ -116,7 +116,7 @@ def main() -> None: ) poller.result() - print(f"Analyzer copied successfully!") + print("Analyzer copied successfully!") # [END copy_analyzer] # [START update_and_verify_analyzer] @@ -130,7 +130,7 @@ def main() -> None: tags={"modelType": "model_in_production"}, ) - print(f"Updating target analyzer with production tag...") + print("Updating target analyzer with production tag...") client.update_analyzer(analyzer_id=target_analyzer_id, resource=updated_analyzer) # Verify the update @@ -143,7 +143,7 @@ def main() -> None: # [END update_and_verify_analyzer] # [START delete_copied_analyzers] - print(f"\nCleaning up analyzers...") + print("\nCleaning up analyzers...") try: client.delete_analyzer(analyzer_id=source_analyzer_id) diff --git a/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/sample_grant_copy_auth.py b/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/sample_grant_copy_auth.py index fb618b29c497..b4ef028c34c4 100644 --- a/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/sample_grant_copy_auth.py +++ b/sdk/contentunderstanding/azure-ai-contentunderstanding/samples/sample_grant_copy_auth.py @@ -206,7 +206,7 @@ def main() -> None: resource=source_analyzer, ) poller.result() - print(f" Source analyzer created successfully!") + print(" Source analyzer created successfully!") # Step 2: Grant copy authorization # Authorization must be granted by the source resource before the target resource can copy @@ -214,7 +214,7 @@ def main() -> None: # - The source analyzer ID to be copied # - The target Azure resource ID that is allowed to receive the copy # - The target region where the copy will be performed (optional, defaults to current region) - print(f"\nStep 2: Granting copy authorization from source resource...") + print("\nStep 2: Granting copy authorization from source resource...") print(f" Target Azure Resource ID: {target_resource_id}") print(f" Target Region: {target_region}") @@ -224,7 +224,7 @@ def main() -> None: target_region=target_region, ) - print(f" Authorization granted successfully!") + print(" Authorization granted successfully!") print(f" Target Azure Resource ID: {copy_auth.target_azure_resource_id}") print(f" Target Region: {target_region}") print(f" Expires at: {copy_auth.expires_at}") @@ -233,7 +233,7 @@ def main() -> None: # The copy_analyzer method must be called on the target client because the target # resource is the one receiving and creating the copy. The target resource validates # that authorization was previously granted by the source resource. - print(f"\nStep 3: Copying analyzer from source to target...") + print("\nStep 3: Copying analyzer from source to target...") print(f" Source Analyzer ID: {source_analyzer_id}") print(f" Source Azure Resource ID: {source_resource_id}") print(f" Source Region: {source_region}") @@ -246,20 +246,20 @@ def main() -> None: source_region=source_region, ) copy_poller.result() - print(f" Analyzer copied successfully to target resource!") + print(" Analyzer copied successfully to target resource!") # Step 4: Verify the copy # Retrieve the analyzer from the target resource to verify the copy was successful - print(f"\nStep 4: Verifying the copied analyzer...") + print("\nStep 4: Verifying the copied analyzer...") copied_analyzer = target_client.get_analyzer(analyzer_id=target_analyzer_id) print(f" Target Analyzer ID: {copied_analyzer.analyzer_id}") print(f" Description: {copied_analyzer.description}") print(f" Status: {copied_analyzer.status}") - print(f"\nCross-resource copy completed successfully!") + print("\nCross-resource copy completed successfully!") finally: # Clean up: Delete both source and target analyzers - print(f"\nCleaning up...") + print("\nCleaning up...") try: source_client.delete_analyzer(analyzer_id=source_analyzer_id) print(f" Source analyzer '{source_analyzer_id}' deleted.") From 149fc11b29dd726c87e9d398454c13b3d29dc70f Mon Sep 17 00:00:00 2001 From: chienyuanchang Date: Wed, 15 Apr 2026 11:54:48 -0700 Subject: [PATCH 12/13] update release date --- .../azure-ai-contentunderstanding/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/contentunderstanding/azure-ai-contentunderstanding/CHANGELOG.md b/sdk/contentunderstanding/azure-ai-contentunderstanding/CHANGELOG.md index 5c68748e3334..30fa4b3c983c 100644 --- a/sdk/contentunderstanding/azure-ai-contentunderstanding/CHANGELOG.md +++ b/sdk/contentunderstanding/azure-ai-contentunderstanding/CHANGELOG.md @@ -1,6 +1,6 @@ # Release History -## 1.0.2 (2026-04-15) +## 1.0.2 (2026-04-16) ### Bugs Fixed - Exposed `usage` property on `AnalyzeLROPoller` and `AnalyzeAsyncLROPoller` to surface billing and token consumption details (`UsageDetails`) returned by the REST API. Previously the `usage` field in the LRO response envelope was discarded by the generated deserialization callback. From 8cf036731799af98fae26344ed11ed126df5f9d6 Mon Sep 17 00:00:00 2001 From: chienyuanchang Date: Fri, 17 Apr 2026 11:35:40 -0700 Subject: [PATCH 13/13] update release date --- .../azure-ai-contentunderstanding/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/contentunderstanding/azure-ai-contentunderstanding/CHANGELOG.md b/sdk/contentunderstanding/azure-ai-contentunderstanding/CHANGELOG.md index 30fa4b3c983c..c1ca68646dc0 100644 --- a/sdk/contentunderstanding/azure-ai-contentunderstanding/CHANGELOG.md +++ b/sdk/contentunderstanding/azure-ai-contentunderstanding/CHANGELOG.md @@ -1,6 +1,6 @@ # Release History -## 1.0.2 (2026-04-16) +## 1.0.2 (2026-04-20) ### Bugs Fixed - Exposed `usage` property on `AnalyzeLROPoller` and `AnalyzeAsyncLROPoller` to surface billing and token consumption details (`UsageDetails`) returned by the REST API. Previously the `usage` field in the LRO response envelope was discarded by the generated deserialization callback.