From 70f36779fb1ffa2463b11d89b9ee9d22cea0a1f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Verdier?= Date: Mon, 10 Aug 2026 15:52:15 +0200 Subject: [PATCH] fix(otel): relax semantic-conventions upper bound The `<0.61` cap pinned the whole OpenTelemetry stack for downstream consumers, not just semconv. OTel ships semconv and the instrumentation packages as a lockstep set pinned with `==`, and semconv itself requires an exact opentelemetry-api version, so the ceiling held consumers' api and sdk at the 1.39.x generation. Replace the ceiling with a floor on semantic-conventions and instrumentation-httpx. All 39 semconv symbols the SDK references resolve to identical values in 0.60b1 and 0.65b0, so no span attribute names change. Lifting the cap surfaced a latent bug. W3C trace-context level 2 adds the random-trace-id flag (0x02), so a sampled span serializes as "-03" from opentelemetry-api 1.44.0. TraceparentInjectionHook compared the flags byte with `endswith("-01")`, read that as unsampled, and discarded the propagated context, starting a new trace on every workflow /execute call. Test the sampled bit as a bitmask instead. Verified on semconv 0.60b1 / api 1.39.1 and semconv 0.65b0 / api 1.44.0. Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe --- pyproject.toml | 4 ++-- src/mistralai/client/_hooks/traceparent.py | 15 ++++++++++++++- .../extra/tests/test_traceparent_hook.py | 2 +- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6c6ee465..fe683c28 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.60b1)", "jsonpath-python >=1.0.6", # required for speakeasy generated path with pagination ] @@ -68,7 +68,7 @@ dev = [ "pyyaml>=6.0.2,<7", "mypy==1.15.0", "msgpack>=1.1.0,<2.0.0", - "opentelemetry-instrumentation-httpx (>=0.60b1,<0.61)", + "opentelemetry-instrumentation-httpx (>=0.60b1)", "opentelemetry-sdk (>=1.33.1,<2.0.0)", "opentelemetry-exporter-otlp-proto-http (>=1.33.1,<2.0.0)", "pylint==3.2.3", diff --git a/src/mistralai/client/_hooks/traceparent.py b/src/mistralai/client/_hooks/traceparent.py index a8ddedf4..f500789e 100644 --- a/src/mistralai/client/_hooks/traceparent.py +++ b/src/mistralai/client/_hooks/traceparent.py @@ -12,6 +12,19 @@ "execute_workflow_registration_v1_workflows_registrations__workflow_registration_id__execute_post", } +_SAMPLED_FLAG = 0x01 + + +# https://www.w3.org/TR/trace-context/#traceparent-header +def _is_sampled(traceparent: str) -> bool: + parts = traceparent.split("-") + if len(parts) != 4: + return False + try: + return bool(int(parts[3], 16) & _SAMPLED_FLAG) + except ValueError: + return False + class TraceparentInjectionHook(BeforeRequestHook): """Inject a sampled traceparent on /execute requests so worker traces are always recorded.""" @@ -29,7 +42,7 @@ def before_request( carrier: Dict[str, str] = {} inject(carrier) traceparent = carrier.get("traceparent", "") - if not traceparent.endswith("-01"): + if not _is_sampled(traceparent): trace_id = random.getrandbits(128) span_id = random.getrandbits(64) traceparent = f"00-{trace_id:032x}-{span_id:016x}-01" diff --git a/src/mistralai/extra/tests/test_traceparent_hook.py b/src/mistralai/extra/tests/test_traceparent_hook.py index 8202f3d6..a5f45a72 100644 --- a/src/mistralai/extra/tests/test_traceparent_hook.py +++ b/src/mistralai/extra/tests/test_traceparent_hook.py @@ -86,7 +86,7 @@ def test_propagates_sampled_active_span(self): assert isinstance(result, httpx.Request) injected = result.headers["traceparent"] - self.assertTrue(injected.endswith("-01")) + self.assertTrue(int(injected.split("-")[3], 16) & 0x01) trace_id_hex = f"{span.get_span_context().trace_id:032x}" self.assertIn(trace_id_hex, injected)