diff --git a/pyproject.toml b/pyproject.toml index 7be2c9e6..b4ea7713 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ dependencies = [ "python-dateutil >=2.8.2", "typing-inspection >=0.4.0", "opentelemetry-api (>=1.33.1,<2.0.0)", - "opentelemetry-semantic-conventions (>=0.60b1,<0.61)", + "opentelemetry-semantic-conventions (>=0.61b0,<0.62)", "jsonpath-python >=1.0.6", # required for speakeasy generated path with pagination ] diff --git a/src/mistralai/extra/observability/otel.py b/src/mistralai/extra/observability/otel.py index 6ea37389..34de57b4 100644 --- a/src/mistralai/extra/observability/otel.py +++ b/src/mistralai/extra/observability/otel.py @@ -251,20 +251,41 @@ def _enrich_response_genai_attrs( # Usage usage = response_data.get("usage", {}) if usage: - attributes.update( - { - gen_ai_attributes.GEN_AI_USAGE_INPUT_TOKENS: usage.get( - "prompt_tokens", 0 - ), - gen_ai_attributes.GEN_AI_USAGE_OUTPUT_TOKENS: usage.get( - "completion_tokens", 0 - ), - } + attributes[gen_ai_attributes.GEN_AI_USAGE_INPUT_TOKENS] = usage.get( + "prompt_tokens", 0 ) + attributes[gen_ai_attributes.GEN_AI_USAGE_OUTPUT_TOKENS] = usage.get( + "completion_tokens", 0 + ) + + cached_input_tokens = _extract_cached_input_tokens(usage) + if cached_input_tokens is not None: + attributes[ + gen_ai_attributes.GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS + ] = cached_input_tokens set_available_attributes(span, attributes) +def _extract_cached_input_tokens(usage: dict[str, Any]) -> int | None: + # The generated usage schema currently exposes both plural/singular + # prompt token details names, plus the legacy top-level cached token count. + # Prefer the nested cached_tokens value when present. + prompt_token_details = usage.get("prompt_tokens_details") or usage.get( + "prompt_token_details" + ) + if isinstance(prompt_token_details, dict): + cached_tokens = prompt_token_details.get("cached_tokens") + if isinstance(cached_tokens, int): + return cached_tokens + + num_cached_tokens = usage.get("num_cached_tokens") + if isinstance(num_cached_tokens, int): + return num_cached_tokens + + return None + + def _enrich_create_agent(span: Span, response_data: dict[str, Any]) -> None: """Set agent-specific attributes from create_agent response. @@ -274,8 +295,7 @@ def _enrich_create_agent(span: Span, response_data: dict[str, Any]) -> None: gen_ai_attributes.GEN_AI_AGENT_DESCRIPTION: response_data.get("description"), gen_ai_attributes.GEN_AI_AGENT_ID: response_data.get("id"), gen_ai_attributes.GEN_AI_AGENT_NAME: response_data.get("name"), - # As of 2026-03-02: in convention, but not yet in opentelemetry-semantic-conventions - "gen_ai.agent.version": str(response_data.get("version")), + gen_ai_attributes.GEN_AI_AGENT_VERSION: str(response_data.get("version")), gen_ai_attributes.GEN_AI_REQUEST_MODEL: response_data.get("model"), gen_ai_attributes.GEN_AI_SYSTEM_INSTRUCTIONS: response_data.get("instructions"), } diff --git a/src/mistralai/extra/tests/test_otel_tracing.py b/src/mistralai/extra/tests/test_otel_tracing.py index ff30ba0c..1aa0dd66 100644 --- a/src/mistralai/extra/tests/test_otel_tracing.py +++ b/src/mistralai/extra/tests/test_otel_tracing.py @@ -272,7 +272,16 @@ def test_simple_chat_completion(self): finish_reason="stop", ), ], - usage=UsageInfo(prompt_tokens=20, completion_tokens=25, total_tokens=45), + usage=UsageInfo.model_validate( + { + "prompt_tokens": 20, + "completion_tokens": 25, + "total_tokens": 45, + "prompt_tokens_details": { + "cached_tokens": 12, + }, + } + ), ) self._run_hook_lifecycle( @@ -301,6 +310,7 @@ def test_simple_chat_completion(self): "gen_ai.response.finish_reasons": ("stop",), "gen_ai.usage.input_tokens": 20, "gen_ai.usage.output_tokens": 25, + "gen_ai.usage.cache_read.input_tokens": 12, }, ) @@ -1390,8 +1400,13 @@ def test_streaming_chat_completion_enriches_span(self): finish_reason="stop", ), ], - usage=UsageInfo( - prompt_tokens=20, completion_tokens=8, total_tokens=28 + usage=UsageInfo.model_validate( + { + "prompt_tokens": 20, + "completion_tokens": 8, + "total_tokens": 28, + "num_cached_tokens": 10, + } ), ), ), @@ -1419,6 +1434,7 @@ def test_streaming_chat_completion_enriches_span(self): "gen_ai.response.model": "mistral-large-latest", "gen_ai.usage.input_tokens": 20, "gen_ai.usage.output_tokens": 8, + "gen_ai.usage.cache_read.input_tokens": 10, "gen_ai.response.finish_reasons": ("stop",), }, ) @@ -1526,7 +1542,6 @@ def failing_tool(x: int) -> str: "Expected an exception event on the span", ) - # -- Baggage propagation: gen_ai.conversation.id --------------------------- def test_conversation_id_from_baggage(self): diff --git a/uv.lock b/uv.lock index 3769cd61..f20e973d 100644 --- a/uv.lock +++ b/uv.lock @@ -620,7 +620,7 @@ requires-dist = [ { name = "jsonpath-python", specifier = ">=1.0.6" }, { name = "mcp", marker = "extra == 'agents'", specifier = ">=1.0,<2.0" }, { name = "opentelemetry-api", specifier = ">=1.33.1,<2.0.0" }, - { name = "opentelemetry-semantic-conventions", specifier = ">=0.60b1,<0.61" }, + { name = "opentelemetry-semantic-conventions", specifier = ">=0.61b0,<0.62" }, { name = "pydantic", specifier = ">=2.11.2" }, { name = "python-dateutil", specifier = ">=2.8.2" }, { name = "requests", marker = "extra == 'gcp'", specifier = ">=2.32.3" }, @@ -710,42 +710,42 @@ wheels = [ [[package]] name = "opentelemetry-api" -version = "1.39.1" +version = "1.40.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "importlib-metadata" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/97/b9/3161be15bb8e3ad01be8be5a968a9237c3027c5be504362ff800fca3e442/opentelemetry_api-1.39.1.tar.gz", hash = "sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c", size = 65767, upload-time = "2025-12-11T13:32:39.182Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/1d/4049a9e8698361cc1a1aa03a6c59e4fa4c71e0c0f94a30f988a6876a2ae6/opentelemetry_api-1.40.0.tar.gz", hash = "sha256:159be641c0b04d11e9ecd576906462773eb97ae1b657730f0ecf64d32071569f", size = 70851, upload-time = "2026-03-04T14:17:21.555Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cf/df/d3f1ddf4bb4cb50ed9b1139cc7b1c54c34a1e7ce8fd1b9a37c0d1551a6bd/opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950", size = 66356, upload-time = "2025-12-11T13:32:17.304Z" }, + { url = "https://files.pythonhosted.org/packages/5f/bf/93795954016c522008da367da292adceed71cca6ee1717e1d64c83089099/opentelemetry_api-1.40.0-py3-none-any.whl", hash = "sha256:82dd69331ae74b06f6a874704be0cfaa49a1650e1537d4a813b86ecef7d0ecf9", size = 68676, upload-time = "2026-03-04T14:17:01.24Z" }, ] [[package]] name = "opentelemetry-sdk" -version = "1.39.1" +version = "1.40.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-semantic-conventions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/eb/fb/c76080c9ba07e1e8235d24cdcc4d125ef7aa3edf23eb4e497c2e50889adc/opentelemetry_sdk-1.39.1.tar.gz", hash = "sha256:cf4d4563caf7bff906c9f7967e2be22d0d6b349b908be0d90fb21c8e9c995cc6", size = 171460, upload-time = "2025-12-11T13:32:49.369Z" } +sdist = { url = "https://files.pythonhosted.org/packages/58/fd/3c3125b20ba18ce2155ba9ea74acb0ae5d25f8cd39cfd37455601b7955cc/opentelemetry_sdk-1.40.0.tar.gz", hash = "sha256:18e9f5ec20d859d268c7cb3c5198c8d105d073714db3de50b593b8c1345a48f2", size = 184252, upload-time = "2026-03-04T14:17:31.87Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/98/e91cf858f203d86f4eccdf763dcf01cf03f1dae80c3750f7e635bfa206b6/opentelemetry_sdk-1.39.1-py3-none-any.whl", hash = "sha256:4d5482c478513ecb0a5d938dcc61394e647066e0cc2676bee9f3af3f3f45f01c", size = 132565, upload-time = "2025-12-11T13:32:35.069Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c5/6a852903d8bfac758c6dc6e9a68b015d3c33f2f1be5e9591e0f4b69c7e0a/opentelemetry_sdk-1.40.0-py3-none-any.whl", hash = "sha256:787d2154a71f4b3d81f20524a8ce061b7db667d24e46753f32a7bc48f1c1f3f1", size = 141951, upload-time = "2026-03-04T14:17:17.961Z" }, ] [[package]] name = "opentelemetry-semantic-conventions" -version = "0.60b1" +version = "0.61b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/91/df/553f93ed38bf22f4b999d9be9c185adb558982214f33eae539d3b5cd0858/opentelemetry_semantic_conventions-0.60b1.tar.gz", hash = "sha256:87c228b5a0669b748c76d76df6c364c369c28f1c465e50f661e39737e84bc953", size = 137935, upload-time = "2025-12-11T13:32:50.487Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/c0/4ae7973f3c2cfd2b6e321f1675626f0dab0a97027cc7a297474c9c8f3d04/opentelemetry_semantic_conventions-0.61b0.tar.gz", hash = "sha256:072f65473c5d7c6dc0355b27d6c9d1a679d63b6d4b4b16a9773062cb7e31192a", size = 145755, upload-time = "2026-03-04T14:17:32.664Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/5e/5958555e09635d09b75de3c4f8b9cae7335ca545d77392ffe7331534c402/opentelemetry_semantic_conventions-0.60b1-py3-none-any.whl", hash = "sha256:9fa8c8b0c110da289809292b0591220d3a7b53c1526a23021e977d68597893fb", size = 219982, upload-time = "2025-12-11T13:32:36.955Z" }, + { url = "https://files.pythonhosted.org/packages/b2/37/cc6a55e448deaa9b27377d087da8615a3416d8ad523d5960b78dbeadd02a/opentelemetry_semantic_conventions-0.61b0-py3-none-any.whl", hash = "sha256:fa530a96be229795f8cef353739b618148b0fe2b4b3f005e60e262926c4d38e2", size = 231621, upload-time = "2026-03-04T14:17:19.33Z" }, ] [[package]]