Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
f330746
add usage
chienyuanchang Apr 10, 2026
7fb940c
add usage into sample
chienyuanchang Apr 13, 2026
6fdd96c
update version for release
chienyuanchang Apr 13, 2026
7976f69
remove unused prints
chienyuanchang Apr 13, 2026
1915f56
Merge branch 'main' into cu-sdk/add_usage
chienyuanchang Apr 13, 2026
ccd7269
add more error type, use is not None, remove dead test code
chienyuanchang Apr 13, 2026
396f7e0
remove trailing line
chienyuanchang Apr 13, 2026
23348c2
Merge branch 'main' into cu-sdk/add_usage
chienyuanchang Apr 14, 2026
bc73756
Merge branch 'main' into cu-sdk/add_usage
chienyuanchang Apr 14, 2026
50250f3
info for token, handles the only legitimate None case for getting usage
chienyuanchang Apr 14, 2026
54f5c2b
fix incorrect note in sample_grant_copy_auth
chienyuanchang Apr 14, 2026
7aaf4e2
Merge branch 'main' into cu-sdk/add_usage
chienyuanchang Apr 14, 2026
f092008
update release date to 2026-04-15
chienyuanchang Apr 14, 2026
82cfe4e
Merge branch 'main' into cu-sdk/add_usage
chienyuanchang Apr 14, 2026
611735f
Updtae test, add async test
chienyuanchang Apr 14, 2026
ec9873e
remove unnecessary f-strings
chienyuanchang Apr 14, 2026
884e433
Merge branch 'main' into cu-sdk/add_usage
chienyuanchang Apr 15, 2026
149fc11
update release date
chienyuanchang Apr 15, 2026
bd14da1
Merge branch 'main' into cu-sdk/add_usage
chienyuanchang Apr 16, 2026
addf5d0
Merge branch 'main' into cu-sdk/add_usage
chienyuanchang Apr 16, 2026
95b9de8
Merge branch 'main' into cu-sdk/add_usage
chienyuanchang Apr 17, 2026
8cf0367
update release date
chienyuanchang Apr 17, 2026
e299fa5
Merge branch 'main' into cu-sdk/add_usage
chienyuanchang Apr 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Release History

## 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.

## 1.0.1 (2026-03-06)

### Bugs Fixed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 |

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -62,6 +64,26 @@ 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
"""
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
"""Wrap an existing AsyncLROPoller without re-initializing the polling method.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -155,6 +157,26 @@ 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
"""
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
"""Wrap an existing LROPoller without re-initializing the polling method.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,37 @@ async def main() -> None:
)
# [END extract_invoice_fields]

# [START get_usage]
# Access usage details from the poller (available after result() completes).
# 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:")
if usage.document_pages_standard is not None:
print(f" Document pages (standard): {usage.document_pages_standard}")
if usage.contextualization_tokens is not None:
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()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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}"
)

Expand All @@ -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]
Expand All @@ -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(
Expand Down Expand Up @@ -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]
Expand All @@ -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"
)

Expand All @@ -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"
)

Expand All @@ -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]
Expand All @@ -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(
Expand Down Expand Up @@ -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]
Expand All @@ -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"
)

Expand All @@ -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"
)

Expand All @@ -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]
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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
)
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -208,15 +208,15 @@ 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
# The grant_copy_authorization method takes:
# - 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}")

Expand All @@ -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}")
Expand All @@ -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}")
Expand All @@ -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
)
Expand Down
Loading