From 98aeb775d2c42e31cffbf07aa44b548019d0f10a Mon Sep 17 00:00:00 2001 From: Manasjyoti Sharma Date: Sat, 27 Jun 2026 19:11:08 +0530 Subject: [PATCH] chore: clean public-facing comments and test docs --- .../instrumentation/anthropic/__init__.py | 10 ++-- .../anthropic/retry_handler.py | 45 +++++++--------- .../tests/conftest.py | 6 +-- .../tests/test_retry_attempt_emission.py | 20 +++---- .../instrumentation/bedrock/__init__.py | 11 ++-- .../instrumentation/bedrock/retry_handler.py | 38 ++++++------- .../tests/test_retry_attempt_emission.py | 16 +++--- .../fortifyroot/retry_registry.py | 22 ++++---- .../tests/test_retry_registry.py | 8 +-- .../instrumentation/langchain/__init__.py | 20 ++++--- .../langchain/callback_handler.py | 10 ++-- .../langchain/retry_handler.py | 43 +++++++-------- .../instrumentation/langchain/span_utils.py | 2 +- .../tests/conftest.py | 4 +- .../tests/test_retry_attempt_emission.py | 18 +++---- .../instrumentation/litellm/__init__.py | 51 +++++++++--------- .../tests/test_logger_integration.py | 2 +- .../tests/test_retry_attempt_emission.py | 16 +++--- .../llamaindex/dispatcher_wrapper.py | 4 +- .../llamaindex/retry_handler.py | 21 ++++---- .../instrumentation/llamaindex/safety.py | 4 +- .../instrumentation/llamaindex/span_utils.py | 6 +-- .../tests/test_retry_attempt_emission.py | 6 +-- .../instrumentation/openai/retry_handler.py | 54 +++++++++---------- .../openai/shared/chat_wrappers.py | 7 ++- .../instrumentation/openai/v1/__init__.py | 4 +- .../tests/conftest.py | 8 +-- .../tests/test_retry_attempt_emission.py | 28 +++++----- .../test_streaming_latency_span_attrs.py | 2 +- .../tests/traces/test_chat.py | 2 +- .../tests/traces/test_completions.py | 2 +- .../tests/traces/test_embeddings.py | 2 +- .../tests/traces/test_responses.py | 4 +- .../tests/test_association_properties.py | 4 +- .../tests/test_privacy_no_prompts.py | 2 +- .../tests/test_prompt_management.py | 2 +- .../tests/test_sdk_initialization.py | 2 +- .../traceloop-sdk/tests/test_workflows.py | 2 +- 38 files changed, 243 insertions(+), 265 deletions(-) diff --git a/packages/opentelemetry-instrumentation-anthropic/opentelemetry/instrumentation/anthropic/__init__.py b/packages/opentelemetry-instrumentation-anthropic/opentelemetry/instrumentation/anthropic/__init__.py index aed9eac54a..24bf0bee8c 100644 --- a/packages/opentelemetry-instrumentation-anthropic/opentelemetry/instrumentation/anthropic/__init__.py +++ b/packages/opentelemetry-instrumentation-anthropic/opentelemetry/instrumentation/anthropic/__init__.py @@ -557,7 +557,7 @@ def _wrap( kwargs = _apply_prompt_safety(span, kwargs, name) _handle_input(span, event_logger, kwargs) start_time = time.perf_counter() - # ST-10.4 (review-driven 2026-05-17): make the anthropic.chat span + # Make the anthropic.chat span # the AMBIENT OTel context for the duration of the SDK call so the # FortifyRoot retry handler (wrapping # ``anthropic._base_client.SyncHttpxClientWrapper.send``) can @@ -698,7 +698,7 @@ async def _awrap( kwargs = await asyncio.to_thread(_apply_prompt_safety, span, kwargs, name) # FR: async safety await _ahandle_input(span, event_logger, kwargs) start_time = time.perf_counter() - # ST-10.4 (review-driven 2026-05-17): see sync _wrap above for + # See sync _wrap above for # rationale on use_span(end_on_exit=False) around the wrapped call. try: with trace.use_span(span, end_on_exit=False): @@ -898,16 +898,16 @@ def _instrument(self, **kwargs): except Exception: pass # that's ok, we don't want to fail if some methods do not exist - # ST-10.4: per-attempt retry_attempt emission via private + # Per-attempt retry_attempt emission via private # ``anthropic._base_client`` httpx wrapper classes. Guarded # against missing private symbols (logs warning + skips emission). # Pass the same tracer_provider the rest of the instrumentor # uses so retry_attempt spans land in the same exporter as the - # anthropic logical span (review-driven 2026-05-16 fix). + # anthropic logical span. instrument_retry_emitter(tracer_provider=tracer_provider) def _uninstrument(self, **kwargs): - uninstrument_retry_emitter() # ST-10.4 symmetry + uninstrument_retry_emitter() # retry-emitter symmetry for wrapped_method in WRAPPED_METHODS: wrap_package = wrapped_method.get("package") wrap_object = wrapped_method.get("object") diff --git a/packages/opentelemetry-instrumentation-anthropic/opentelemetry/instrumentation/anthropic/retry_handler.py b/packages/opentelemetry-instrumentation-anthropic/opentelemetry/instrumentation/anthropic/retry_handler.py index 8b21b2f11c..3b79c7d9c2 100644 --- a/packages/opentelemetry-instrumentation-anthropic/opentelemetry/instrumentation/anthropic/retry_handler.py +++ b/packages/opentelemetry-instrumentation-anthropic/opentelemetry/instrumentation/anthropic/retry_handler.py @@ -1,7 +1,6 @@ -"""ST-10.4 retry-aware emission for the Anthropic direct SDK. +"""Retry-aware emission for the Anthropic direct SDK. -Per RETRY_LOOP.md §4.4 Anthropic row + §4.7 suppression discipline + -ST-10.0 hook-table addendum (in phase_st10_retryloop.txt): +Design contract: - Hook ``anthropic._base_client.SyncHttpxClientWrapper.send`` and ``AsyncHttpxClientWrapper.send`` — fires once per HTTP attempt @@ -16,17 +15,16 @@ - Endpoint allow-list: only LLM endpoints emit retry_attempt spans (``/v1/messages`` for the modern Messages API, ``/v1/complete`` for legacy completions). Anything else (token - refresh, model listing, etc.) does NOT emit retry_attempt — per - §4.4.1 allow-listing requirement. - - Suppression discipline (§4.7): check BOTH the OTel context + refresh, model listing, etc.) does NOT emit retry_attempt. + - Suppression discipline: check BOTH the OTel context ``SUPPRESS_LANGUAGE_MODEL_INSTRUMENTATION_KEY`` AND the shared ``is_framework_owned()`` registry. If either says "suppress", skip emission. Prevents framework retries (LiteLLM / LangChain / LlamaIndex) from DOUBLE-emitting when both framework wrappers and direct-SDK wrappers are active. - Parent resolution: use the current OTel ambient span. If invalid - or absent, skip gracefully (no orphan retry_attempt span). The - §4.5 backend dedup degrades gracefully. + or absent, skip gracefully (no orphan retry_attempt span). Backend + dedup degrades gracefully. Tests: see ``tests/test_retry_attempt_emission.py``. """ @@ -54,7 +52,7 @@ logger = logging.getLogger(__name__) -# ST-10 §4.4 / §4.5 constants. +# Retry-attempt constants. _FR_SPAN_ROLE_KEY = "fortifyroot.span.role" _FR_LLM_ATTEMPT_SPAN_NAME_PREFIX = "fortifyroot.anthropic" _FR_SPAN_ROLE_LLM_ATTEMPT = "llm_attempt" @@ -199,9 +197,7 @@ def _start_attempt_span(request: Any, parent_span: "trace.Span") -> "trace.Span" # ``anthropic/__init__.py`` ``_wrap``). The earlier draft of # this handler used lower-case "anthropic" for cross-handler # consistency, but that mismatched the upstream Anthropic - # instrumentor and the fr-system-tests ProviderModel - # ``gen_ai_system="Anthropic"`` assertion — ST-10.4 - # review-driven fix 2026-05-17. Backend provider grouping is + # instrumentor and provider-model assertions. Backend provider grouping is # case-sensitive in practice; canonicalisation happens via # ``event_provider`` (which is always lower-case in tests). "gen_ai.system": "Anthropic", @@ -229,11 +225,11 @@ def _finalize_success(span: "trace.Span", response: Any, *, is_streaming: bool = For non-streaming responses (regardless of 2xx vs non-2xx), parse the JSON body and extract usage tokens + response id + response - model. Per RETRY_LOOP.md §4.4 token-usage rule (around line 164): + model. Per the retry-attempt token-usage rule: wrappers MUST extract usage from the response body whenever it's present, regardless of whether the attempt succeeded — some failures DO consume tokens and the provider returns usage in the - error body. §4.5 backend dedup makes the qualifying retry_attempt + error body. Backend dedup makes the qualifying retry_attempt canonical (even single-attempt), so usage must live on this span. Streaming responses (``stream=True`` passed to ``send()``) skip @@ -257,7 +253,7 @@ def _finalize_success(span: "trace.Span", response: Any, *, is_streaming: bool = pass # Non-streaming body parse — applies to BOTH 2xx and non-2xx per the - # §4.4 token-usage rule. ``_extract_usage_from_body`` is fully + # retry-attempt token-usage rule. ``_extract_usage_from_body`` is fully # defensive (missing fields / parse failure / no ``usage`` block all # degrade silently). if not is_streaming: @@ -348,22 +344,21 @@ def _should_emit_for(request: Any) -> bool: def _sync_send_wrapper(wrapped, instance, args, kwargs): """Wraps ``anthropic._base_client.SyncHttpxClientWrapper.send``. - Direct-SDK wrappers do NOT register tokens in the §4.7.1 framework + Direct-SDK wrappers do NOT register tokens in the framework-attempt registry (the registry's contract reserves registration for FRAMEWORK wrappers — LiteLLM / LangChain / LlamaIndex). They only consult via ``is_framework_owned()``. Self-registering would falsely suppress concurrent direct-SDK calls on the same OS thread, which manifests most visibly under asyncio (multiple tasks sharing one thread). - Streaming skip (ST-10.4 review-driven 2026-05-17): when + Streaming skip: when ``stream=True`` is passed to send, this wrapper SKIPS retry_attempt emission. Streaming retry_attempts cannot carry usage (SSE stream - can't be peeked) but §4.5 dedup would still promote them to the + can't be peeked) but backend dedup would still promote them to the canonical LLMUsageEvent, producing zero-token events. Leaving the parent ``anthropic.chat`` span as the canonical (it gets full usage from the Anthropic streaming wrapper). Streaming retry-loop - detection is the deferred follow-up - ``ST-10.4-FOLLOWUP-streaming-usage``. + detection is a deferred streaming-usage follow-up. """ request = args[0] if args else kwargs.get("request") if request is None or not _should_emit_for(request): @@ -490,7 +485,7 @@ def instrument_retry_emitter(tracer_provider=None) -> None: ) if not sync_ok: logger.warning( - "ST-10.4: anthropic._base_client.%s.%s missing/incompatible; " + "Anthropic retry emitter: anthropic._base_client.%s.%s missing/incompatible; " "skipping sync retry_attempt emission. Normal anthropic " "instrumentation is unaffected.", _SYNC_WRAPPER_CLASS, _WRAPPED_METHOD, @@ -504,13 +499,13 @@ def instrument_retry_emitter(tracer_provider=None) -> None: ) except Exception as e: logger.warning( - "ST-10.4: failed to wrap anthropic sync httpx send (%s); " + "Anthropic retry emitter: failed to wrap anthropic sync httpx send (%s); " "retry_attempt emission disabled for sync path", e, ) if not async_ok: logger.warning( - "ST-10.4: anthropic._base_client.%s.%s missing/incompatible; " + "Anthropic retry emitter: anthropic._base_client.%s.%s missing/incompatible; " "skipping async retry_attempt emission. Normal anthropic " "instrumentation is unaffected.", _ASYNC_WRAPPER_CLASS, _WRAPPED_METHOD, @@ -524,7 +519,7 @@ def instrument_retry_emitter(tracer_provider=None) -> None: ) except Exception as e: logger.warning( - "ST-10.4: failed to wrap anthropic async httpx send (%s); " + "Anthropic retry emitter: failed to wrap anthropic async httpx send (%s); " "retry_attempt emission disabled for async path", e, ) @@ -543,7 +538,7 @@ def uninstrument_retry_emitter() -> None: unwrap(f"{_ANTHROPIC_BASE_CLIENT_MODULE}.{cls}", _WRAPPED_METHOD) except Exception: logger.debug( - "ST-10.4: anthropic unwrap of %s.%s failed (likely " + "Anthropic retry emitter: unwrap of %s.%s failed (likely " "wrap was never installed for this variant)", cls, _WRAPPED_METHOD, exc_info=True, diff --git a/packages/opentelemetry-instrumentation-anthropic/tests/conftest.py b/packages/opentelemetry-instrumentation-anthropic/tests/conftest.py index 94e9aef44e..dfc736d76a 100644 --- a/packages/opentelemetry-instrumentation-anthropic/tests/conftest.py +++ b/packages/opentelemetry-instrumentation-anthropic/tests/conftest.py @@ -29,10 +29,10 @@ class _NoFortifyRootSpanExporter(InMemorySpanExporter): - """ST-10.4 (2026-05-17): filter out any span whose name starts + """FortifyRoot retry-attempt (2026-05-17): filter out any span whose name starts with ``fortifyroot.`` from the upstream-test span exporter. - ST-10.4 added per-attempt ``fortifyroot.anthropic.attempt_1`` + FortifyRoot retry-attempt added per-attempt ``fortifyroot.anthropic.attempt_1`` sibling spans under every anthropic logical call (once the Anthropic ``_wrap`` started using ``trace.use_span`` so the retry handler can find the parent span). Upstream / legacy Anthropic @@ -42,7 +42,7 @@ class _NoFortifyRootSpanExporter(InMemorySpanExporter): retry_attempt sibling is also exported. Mirrors the OpenAI test conftest pattern (added 2026-05-16) and - the LangChain CI-hardening pattern (2026-05-15). ST-10.4 unit + the LangChain CI-hardening pattern (2026-05-15). FortifyRoot retry-attempt unit tests in ``tests/test_retry_attempt_emission.py`` use their own ``fresh_tracer`` fixture (not this one), so they continue to see retry_attempt spans and aren't affected by the filter. diff --git a/packages/opentelemetry-instrumentation-anthropic/tests/test_retry_attempt_emission.py b/packages/opentelemetry-instrumentation-anthropic/tests/test_retry_attempt_emission.py index 5028293ab5..25f451d7b1 100644 --- a/packages/opentelemetry-instrumentation-anthropic/tests/test_retry_attempt_emission.py +++ b/packages/opentelemetry-instrumentation-anthropic/tests/test_retry_attempt_emission.py @@ -1,6 +1,6 @@ -"""Tests for ST-10.4 Anthropic direct-SDK retry-attempt emission. +"""Tests for FortifyRoot retry-attempt Anthropic direct-SDK retry-attempt emission. -Covers (per RETRY_LOOP.md §4.4 Anthropic row + §4.7 suppression): +Covers (per retry-loop design notes §4.4 Anthropic row + §4.7 suppression): - Instrumentor symmetry: install/uninstall flips state cleanly and is idempotent. - Single-attempt happy path: ONE llm_attempt span under the active @@ -101,7 +101,7 @@ def _make_response(status_code: int = 200, request_id: str = "req-abc", body: Optional[dict] = None) -> SimpleNamespace: """Fake ``httpx.Response``. Non-streaming retry_attempt path now parses the body for usage / response.id / response.model — see the - 2026-05-13 review-driven C2 fix. + 2026-05-13 retry-parent resolution fix. """ if body is None: body = { @@ -382,7 +382,7 @@ def test_framework_registry_suppression_skips_emission(fresh_tracer): def test_direct_sdk_wrapper_does_not_self_register_in_framework_registry(fresh_tracer): - """REGRESSION GUARD (review-driven fix 2026-05-13): direct-SDK + """REGRESSION GUARD: direct-SDK wrappers MUST NOT register tokens in the §4.7.1 framework registry. Self-registration causes false suppression of concurrent direct-SDK calls sharing the same thread (asyncio repro in @@ -409,7 +409,7 @@ def wrapped(*a, **kw): def test_two_concurrent_async_sends_each_emit_a_retry_attempt(fresh_tracer): - """REGRESSION GUARD (review-driven fix 2026-05-13): two asyncio + """REGRESSION GUARD: two asyncio tasks sharing one OS thread MUST each emit their own retry_attempt.""" tracer, exporter, _ = fresh_tracer @@ -477,12 +477,12 @@ def test_llm_endpoints_emit_with_correct_operation(fresh_tracer, path, expected_ # --------------------------------------------------------------------------- -# §4.5-driven usage-extraction policy (review-driven fix 2026-05-13). +# §4.5-driven usage-extraction policy. # --------------------------------------------------------------------------- def test_non_streaming_response_body_populates_usage_id_and_model(fresh_tracer): """Backend dedup makes the retry_attempt span the canonical - LLMUsageEvent (see ``proc_llm_extractor.go``). Anthropic non-streaming + LLMUsageEvent (see ``backend LLM extractor``). Anthropic non-streaming responses MUST carry usage on the retry_attempt span.""" tracer, exporter, _ = fresh_tracer request = _make_request(model="claude-haiku-4-5") @@ -510,7 +510,7 @@ def test_non_streaming_response_body_populates_usage_id_and_model(fresh_tracer): def test_streaming_request_skips_emission_entirely(fresh_tracer): - """ST-10.4 (review-driven 2026-05-17): when ``stream=True`` is + """FortifyRoot retry-attempt: when ``stream=True`` is passed to send, the wrap SKIPS retry_attempt emission entirely. See the openai analog for full rationale. The parent ``anthropic.chat`` span stays the canonical LLMUsageEvent. @@ -551,8 +551,8 @@ def explode(): def test_non_2xx_response_with_usage_in_body_populates_usage_tokens(fresh_tracer): - """REGRESSION GUARD (review-driven follow-up 2026-05-13): per - RETRY_LOOP.md §4.4 token-usage rule, wrappers MUST extract usage + """REGRESSION GUARD: per + retry-loop design notes §4.4 token-usage rule, wrappers MUST extract usage from the response body whenever present, regardless of success. Some failures consume tokens and the provider returns usage in the error body.""" diff --git a/packages/opentelemetry-instrumentation-bedrock/opentelemetry/instrumentation/bedrock/__init__.py b/packages/opentelemetry-instrumentation-bedrock/opentelemetry/instrumentation/bedrock/__init__.py index 1bb4e8187d..f9274b90db 100644 --- a/packages/opentelemetry-instrumentation-bedrock/opentelemetry/instrumentation/bedrock/__init__.py +++ b/packages/opentelemetry-instrumentation-bedrock/opentelemetry/instrumentation/bedrock/__init__.py @@ -131,8 +131,7 @@ def is_metrics_enabled() -> bool: def _with_tracer_wrapper(func): """Helper for providing tracer for wrapper functions. - ``tracer_provider`` (added 2026-05-16 for the ST-10.4 review-driven - fix) is plumbed alongside the tracer so the bedrock-runtime client + ``tracer_provider`` is plumbed alongside the tracer so the bedrock-runtime client wrap can pass it to ``install_event_hooks_on_client`` — letting the retry handler's event hooks emit spans through the same provider the rest of the instrumentor uses. @@ -201,12 +200,12 @@ def _wrap( client.converse_stream = _instrumented_converse_stream( client.converse_stream, tracer, metric_params, event_logger ) - # ST-10.4: register per-attempt botocore event hooks on the + # Register per-attempt botocore event hooks on the # bedrock-runtime client. Public botocore API; emits one # retry_attempt sibling span per HTTP attempt under the outer # bedrock.completion / bedrock.converse span. ``tracer_provider`` # is passed so retry_attempt spans go to the same provider as - # the bedrock logical span — review-driven 2026-05-16 fix. + # the bedrock logical span. install_event_hooks_on_client(client, tracer_provider=tracer_provider) return client except Exception as e: @@ -254,7 +253,7 @@ def with_instrumentation(*args, **kwargs): span = tracer.start_span(_BEDROCK_INVOKE_SPAN_NAME, kind=SpanKind.CLIENT) - # ST-10.4: make the streaming span the AMBIENT OTel context for + # Make the streaming span the AMBIENT OTel context for # the duration of the underlying boto3 call so per-attempt # botocore event hooks (before-send.bedrock-runtime.*) can # resolve this span as the retry_attempt parent. The span is @@ -304,7 +303,7 @@ def with_instrumentation(*args, **kwargs): span = tracer.start_span(_BEDROCK_CONVERSE_SPAN_NAME, kind=SpanKind.CLIENT) kwargs = _apply_converse_prompt_safety(span, kwargs, _BEDROCK_CONVERSE_SPAN_NAME) - # ST-10.4: see _instrumented_model_invoke_with_response_stream + # See _instrumented_model_invoke_with_response_stream # for the rationale on use_span(end_on_exit=False). stream_start_time = time.perf_counter() with trace.use_span(span, end_on_exit=False): diff --git a/packages/opentelemetry-instrumentation-bedrock/opentelemetry/instrumentation/bedrock/retry_handler.py b/packages/opentelemetry-instrumentation-bedrock/opentelemetry/instrumentation/bedrock/retry_handler.py index 5b59ee6be8..4c343d69ad 100644 --- a/packages/opentelemetry-instrumentation-bedrock/opentelemetry/instrumentation/bedrock/retry_handler.py +++ b/packages/opentelemetry-instrumentation-bedrock/opentelemetry/instrumentation/bedrock/retry_handler.py @@ -1,7 +1,6 @@ -"""ST-10.4 retry-aware emission for the Bedrock (botocore) direct SDK. +"""Retry-aware emission for the Bedrock (botocore) direct SDK. -Per RETRY_LOOP.md §4.4 Bedrock row + §4.7 suppression discipline + -ST-10.0 hook-table addendum (in phase_st10_retryloop.txt): +Design contract: - Use botocore's PUBLIC event-hook API on bedrock-runtime clients: * ``before-send.bedrock-runtime.*`` fires once per HTTP attempt @@ -23,14 +22,13 @@ do NOT register a framework-attempt token here — the §4.7.1 registry's documented contract reserves registration for FRAMEWORK wrappers (LiteLLM / LangChain / LlamaIndex). Direct-SDK wrappers - only CONSULT via ``is_framework_owned()``. (See the 2026-05-13 - review-driven C1 fix; an earlier draft of this module incorrectly - registered a token.) + only CONSULT via ``is_framework_owned()``. An earlier draft of + this module incorrectly registered a token. - Endpoint allow-list: implicit via the event-name pattern ``bedrock-runtime.*``. Only bedrock-runtime operations (Invoke / Converse and their streaming variants) fire these events; non-LLM AWS service traffic (S3, STS, etc.) is unaffected. - - Suppression discipline (§4.7): before emitting, check BOTH the + - Suppression discipline: before emitting, check BOTH the OTel context ``SUPPRESS_LANGUAGE_MODEL_INSTRUMENTATION_KEY`` AND the shared ``is_framework_owned()`` registry (consult-only — see above). @@ -69,7 +67,7 @@ logger = logging.getLogger(__name__) -# ST-10 §4.4 / §4.5 constants. +# Retry-attempt constants. _FR_SPAN_ROLE_KEY = "fortifyroot.span.role" _FR_LLM_ATTEMPT_SPAN_NAME_PREFIX = "fortifyroot.bedrock" _FR_SPAN_ROLE_LLM_ATTEMPT = "llm_attempt" @@ -93,7 +91,7 @@ # the parent span on their provider but the retry_attempt span sent # into a no-op tracer. Module-level (not per-client) because all # bedrock-runtime clients created under one instrumentor share the -# same provider config. (review-driven 2026-05-16 fix.) +# same provider config. _tracer_provider = None @@ -309,14 +307,14 @@ def _before_send_hook(event_name: Optional[str] = None, request: Any = None, **_ ``request.context`` for retrieval by the paired ``response-received`` hook. - No self-registration in the §4.7.1 framework registry: the registry's + No self-registration in the framework-attempt registry: the registry's contract reserves registration for FRAMEWORK wrappers (LiteLLM / LangChain / LlamaIndex). Direct-SDK wrappers only CONSULT via ``is_framework_owned()``. Defensive cleanup of any prior context-stored span on this request still runs, but it no longer touches the framework registry. - Streaming skip (ST-10.4 review-driven 2026-05-17): when the + Streaming skip: when the botocore operation is a streaming one (event name ends with ``Stream``: ``InvokeModelWithResponseStream`` / ``ConverseStream``), this hook skips retry_attempt emission. @@ -324,12 +322,11 @@ def _before_send_hook(event_name: Optional[str] = None, request: Any = None, **_ cannot carry token usage at attempt-end (usage arrives via the stream-completion callback installed by the Bedrock streaming wrapper, AFTER our hook has already finalised the span), but - §4.5 dedup would still promote them to canonical → zero-token + backend dedup would still promote them to canonical → zero-token LLMUsageEvent. Leaving the parent ``bedrock.completion`` / ``bedrock.converse`` span (which DOES get full usage from ``stream_done``) as the canonical event. Streaming retry-loop - detection is the deferred follow-up - ``ST-10.4-FOLLOWUP-streaming-usage``. + detection is a deferred streaming-usage follow-up. """ try: if request is None: @@ -367,7 +364,7 @@ def _before_send_hook(event_name: Optional[str] = None, request: Any = None, **_ _set_parent_marker(parent) ctx[_CTX_SPAN_KEY] = span except Exception: - logger.debug("ST-10.4: bedrock before-send hook failed", exc_info=True) + logger.debug("Bedrock retry emitter: before-send hook failed", exc_info=True) class _ResponseDictAdapter: @@ -413,8 +410,7 @@ def _response_received_hook( * ``http_response`` + ``parsed`` (older botocore signature the fork-side unit tests drive directly). * ``response_dict`` + ``parsed_response`` (botocore 1.42.x — the - names the real event emitter actually uses; verified via - ST-10.6 fr-system-tests probe). + names the real event emitter actually uses). Per-attempt finalisation reads ``status_code`` and ``headers`` from whichever shape is present; the rest of the body just falls through @@ -449,7 +445,7 @@ def _response_received_hook( except Exception: pass except Exception: - logger.debug("ST-10.4: bedrock response-received hook failed", exc_info=True) + logger.debug("Bedrock retry emitter: response-received hook failed", exc_info=True) def install_event_hooks_on_client(client: Any, tracer_provider=None) -> None: @@ -476,7 +472,7 @@ def install_event_hooks_on_client(client: Any, tracer_provider=None) -> None: events = getattr(events, "events", None) if events is not None else None if events is None: logger.debug( - "ST-10.4: bedrock client missing .meta.events; " + "Bedrock retry emitter: client missing .meta.events; " "skipping retry_attempt event-hook registration" ) return @@ -492,7 +488,7 @@ def install_event_hooks_on_client(client: Any, tracer_provider=None) -> None: ) except Exception: logger.warning( - "ST-10.4: failed to register bedrock retry_attempt event hooks; " + "Bedrock retry emitter: failed to register retry_attempt event hooks; " "retry_attempt emission disabled for this client", exc_info=True, ) @@ -529,7 +525,7 @@ def uninstall_event_hooks_on_client(client: Any) -> None: except Exception: pass except Exception: - logger.debug("ST-10.4: bedrock event-hook unregister failed", exc_info=True) + logger.debug("Bedrock retry emitter: event-hook unregister failed", exc_info=True) def _reset_tracer_provider_for_test() -> None: diff --git a/packages/opentelemetry-instrumentation-bedrock/tests/test_retry_attempt_emission.py b/packages/opentelemetry-instrumentation-bedrock/tests/test_retry_attempt_emission.py index b5810a7146..4477571672 100644 --- a/packages/opentelemetry-instrumentation-bedrock/tests/test_retry_attempt_emission.py +++ b/packages/opentelemetry-instrumentation-bedrock/tests/test_retry_attempt_emission.py @@ -1,6 +1,6 @@ -"""Tests for ST-10.4 Bedrock direct-SDK retry-attempt emission. +"""Tests for FortifyRoot retry-attempt Bedrock direct-SDK retry-attempt emission. -Covers (per RETRY_LOOP.md §4.4 Bedrock row + §4.7 suppression): +Covers (per retry-loop design notes §4.4 Bedrock row + §4.7 suppression): - install_event_hooks_on_client registers both event-name patterns on a bedrock-runtime client's botocore event system. - Single-attempt happy path: before-send → response-received pair @@ -384,8 +384,8 @@ def test_framework_registry_suppression_skips_emission(fresh_tracer): def test_direct_sdk_wrapper_does_not_self_register_in_framework_registry(fresh_tracer): - """REGRESSION GUARD (review-driven fix 2026-05-13): the bedrock - retry hooks MUST NOT register tokens in the §4.7.1 framework + """REGRESSION GUARD: the Bedrock retry hooks MUST NOT register + tokens in the §4.7.1 framework registry. Direct-SDK wrappers only CONSULT via ``is_framework_owned()``.""" tracer, _, _ = fresh_tracer @@ -494,11 +494,11 @@ def test_before_send_with_request_missing_context_is_noop(fresh_tracer): # --------------------------------------------------------------------------- -# M1: streaming-span ambient-context fix (review-driven 2026-05-13). +# Streaming-span ambient-context fix. # --------------------------------------------------------------------------- def test_streaming_event_skips_emission_entirely(fresh_tracer): - """ST-10.4 (review-driven 2026-05-17): when the botocore operation + """FortifyRoot retry-attempt: when the botocore operation is a streaming one (event name ends with ``Stream``: ``InvokeModelWithResponseStream`` / ``ConverseStream``), the ``_before_send_hook`` SKIPS retry_attempt emission entirely. @@ -508,11 +508,11 @@ def test_streaming_event_skips_emission_entirely(fresh_tracer): via the stream-completion callback in the Bedrock streaming wrapper, AFTER our hook has finalised) but §4.5 dedup would still promote them to canonical → zero-token LLMUsageEvents - breaking fr-system-tests. The parent + breaking system tests. The parent ``bedrock.completion`` / ``bedrock.converse`` span (which DOES get full usage via ``stream_done``) stays canonical. Streaming retry-loop detection is the deferred follow-up - ``ST-10.4-FOLLOWUP-streaming-usage``. + ``streaming retry-usage follow-up``. This test supersedes the earlier ``test_streaming_span_via_use_span_parents_retry_attempt`` from diff --git a/packages/opentelemetry-instrumentation-fortifyroot/opentelemetry/instrumentation/fortifyroot/retry_registry.py b/packages/opentelemetry-instrumentation-fortifyroot/opentelemetry/instrumentation/fortifyroot/retry_registry.py index a548991ac4..3c11cd9ab6 100644 --- a/packages/opentelemetry-instrumentation-fortifyroot/opentelemetry/instrumentation/fortifyroot/retry_registry.py +++ b/packages/opentelemetry-instrumentation-fortifyroot/opentelemetry/instrumentation/fortifyroot/retry_registry.py @@ -1,4 +1,4 @@ -"""§4.7.1 token-based framework-attempt registry. +"""Token-based framework-attempt registry. Shared across all FR fork instrumentations. The flow is: @@ -23,14 +23,13 @@ the duration of one attempt. (Re-entrancy across threads is handled by the per-TID dict.) -Design references: - - RETRY_LOOP.md §4.7 — suppression discipline rationale. - - RETRY_LOOP.md §4.7.1 — registry shape, eviction policy, required - tests (re-entrancy, stale-cleanup, - parent-end cleanup, cap-eviction, - thread-ID reuse). - - phase_st10_retryloop.txt round-3 disposition — replaces an - earlier ``set[int]`` design that didn't handle re-entrancy. +Design notes: + - The registry owns suppression discipline for framework retry loops. + - The shape, eviction policy, and tests cover re-entrancy, + stale-cleanup, parent-end cleanup, cap-eviction, and thread-ID + reuse. + - The token map replaces an earlier per-thread set design that did + not handle re-entrant attempts. """ from __future__ import annotations @@ -44,7 +43,7 @@ logger = logging.getLogger(__name__) -# Module-level state (per process). Per RETRY_LOOP.md §4.7.1: +# Module-level state (per process): # shape: dict[tid, dict[token, started_at_monotonic_seconds]] # Keying by tid (thread ID) is what makes per-thread ownership work; # keying by token within a TID is what makes re-entrancy work (one @@ -227,8 +226,7 @@ def is_framework_owned(tid: Optional[int] = None) -> bool: truth for that logical call. Performs per-TID stale eviction in-band before answering, so a - leaked token cannot indefinitely suppress emission (see - review-round-4 Q2 fix in phase_st10_retryloop.txt). + leaked token cannot indefinitely suppress emission. """ if tid is None: tid = threading.get_ident() diff --git a/packages/opentelemetry-instrumentation-fortifyroot/tests/test_retry_registry.py b/packages/opentelemetry-instrumentation-fortifyroot/tests/test_retry_registry.py index 1f146ef045..02498ca9f4 100644 --- a/packages/opentelemetry-instrumentation-fortifyroot/tests/test_retry_registry.py +++ b/packages/opentelemetry-instrumentation-fortifyroot/tests/test_retry_registry.py @@ -1,6 +1,6 @@ """Tests for the §4.7.1 framework-attempt registry. -Required tests per RETRY_LOOP.md §4.7.1: +Required tests per retry-loop design notes §4.7.1: - re-entrancy (one thread can hold multiple tokens, suppression only stops when ALL are unregistered) - stale-entry eviction (TTL-based, runs in-band on read path) @@ -111,7 +111,7 @@ def worker(): def test_stale_entry_eviction_via_ttl(monkeypatch): """Tokens older than _REGISTRY_STALE_TTL_SEC are evicted on the next is_framework_owned() call. This is the load-bearing - in-band eviction (review-round-4 Q2 fix) — without it a leaked + in-band eviction path; without it a leaked token (framework crashed) would suppress emission forever.""" # Shrink the TTL so the test runs in real time. monkeypatch.setattr(retry_registry, "_REGISTRY_STALE_TTL_SEC", 0.1) @@ -226,7 +226,9 @@ def thread_two(): def test_unregister_works_from_different_thread_than_register(): - """REGRESSION GUARD (review-batch-1 Blocker fix 2026-05-10): a + """Regression guard for cross-thread unregister behavior. + + A framework's start callback and terminal callback can run on different OS threads (e.g. asyncio dispatching success/failure callbacks to a worker thread). The original implementation used diff --git a/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/__init__.py b/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/__init__.py index 98925bf39c..d28300fe45 100644 --- a/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/__init__.py +++ b/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/__init__.py @@ -102,8 +102,8 @@ def _instrument(self, **kwargs): traceloopCallbackHandler = TraceloopCallbackHandler( tracer, duration_histogram, token_histogram ) - # ST-10.2: register the FR retry-attempt handler alongside the - # existing Traceloop handler. Per ST-10.0 C2 POC findings, this + # Register the FR retry-attempt handler alongside the + # existing Traceloop handler. This # captures per-HTTP-attempt callbacks on framework-layer retry # paths (e.g. Runnable.with_retry) and emits one # fortifyroot.langchain.attempt_ sibling span per attempt @@ -111,7 +111,7 @@ def _instrument(self, **kwargs): # handler as a CONSTRUCTION-time reference so it can resolve the # workflow parent via Traceloop's run_id-keyed ``spans`` dict # without mutating shared state per-callback-manager-init - # (review-round-2 Major 5). + # shared-state races. fortifyrootRetryHandler = _FortifyRootRetryHandler( traceloop_handler=traceloopCallbackHandler, ) @@ -253,10 +253,8 @@ def __call__( # the test-isolation bug observed when the FR retry handler # was registered FIRST: running before Traceloop caused # Traceloop's context-attach/detach discipline to break - # downstream (LiteLLM tests in the same pytest session - # observed stale OTel ambient context leaking from LangChain - # — review-batch-1 v6 trace-id-shared-across-tests bug, - # 2026-05-11). + # downstream tests from observing stale OTel ambient context + # leaking from LangChain. for handler in instance.inheritable_handlers: if isinstance(handler, type(self._callback_handler)): break @@ -266,14 +264,14 @@ def __call__( # we need a way to determine the type of CallbackManager being wrapped. self._callback_handler._callback_manager = instance instance.add_handler(self._callback_handler, True) - # ST-10.2: register the FR retry-attempt handler AFTER + # Register the FR retry-attempt handler AFTER # Traceloop. Idempotent registration. The handler already # holds a CONSTRUCTION-time reference to the Traceloop handler # (set in LangchainInstrumentor._instrument). We deliberately # do NOT mutate any shared attribute on the retry handler per # callback-manager-init — that previously raced when concurrent # Runnable invocations created BaseCallbackManagers on - # different threads (review-round-2 Major 5). + # different threads. if self._retry_handler is not None: for handler in instance.inheritable_handlers: if isinstance(handler, type(self._retry_handler)): @@ -319,13 +317,13 @@ def __call__( # In legacy chains like LLMChain, suppressing model instrumentations # within create_llm_span doesn't work, so this should helps as a fallback. # - # ST-10 review-round-2 fix (2026-05-11): capture the attach token + # Capture the attach token # and detach in ``finally`` so this suppression layer doesn't leak # into the OTel context stack indefinitely. The pre-fix code # never detached, accumulating SUPPRESS_LANGUAGE_MODEL_INSTRUMENTATION_KEY # frames on every wrapped openai call, which compounded across # LangChain tests in long pytest sessions and risked corrupting - # later instrumentor behaviour. See review-round-2 Blocker 3. + # later instrumentor behaviour. suppression_token: Optional[Any] = None try: suppression_token = context_api.attach( diff --git a/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/callback_handler.py b/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/callback_handler.py index 7cb152d254..1fd2e6f1d1 100644 --- a/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/callback_handler.py +++ b/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/callback_handler.py @@ -195,7 +195,7 @@ def _end_span(self, span: Span, run_id: UUID) -> None: if child_span.end_time is None: # avoid warning on ended spans child_span.end() span.end() - # ST-10 review-round-2 fix (2026-05-11): detach ALL attached + # Detach ALL attached # tokens in LIFO order (reverse of attach). Pre-fix, only the # single ``token`` field was detached — which for LLM spans # was the suppression token only, leaving the span-context @@ -273,7 +273,7 @@ def _create_span( entity_path: str = "", metadata: Optional[dict[str, Any]] = None, ) -> Span: - # ST-10 review-round-2 fix (2026-05-11): capture every + # Capture every # context_api.attach()'s return token and append to the # SpanHolder's ``tokens`` list. ``_end_span`` detaches them in # LIFO order. Pre-fix, the metadata-association attach below @@ -284,7 +284,7 @@ def _create_span( # span-context attach, leaving the ended span "current" in OTel # context past the end of the LangChain test, which polluted # later LiteLLM / LlamaIndex tests in the same pytest session - # (session-scoped sdk_helper). See review-round-2 Blocker 1. + # session. attached_tokens: list[Any] = [] if metadata is not None: current_association_properties = ( @@ -404,7 +404,7 @@ def _create_llm_span( # we already have an LLM span by this point, # so skip any downstream instrumentation from here # - # ST-10 review-round-2 fix (2026-05-11): APPEND the suppression + # Append the suppression # token to the existing SpanHolder.tokens list rather than # replacing the SpanHolder. The pre-fix code created a new # SpanHolder with ONLY the suppression token, dropping the @@ -513,7 +513,7 @@ def on_chain_end( self._end_span(span, run_id) if parent_run_id is None: - # ST-10 review-round-2 note (2026-05-11): pre-existing leak — + # Pre-existing leak: # this attach is not paired with a detach. It writes # SUPPRESS_LANGUAGE_MODEL_INSTRUMENTATION_KEY=False to a new # context layer that grows the OTel context stack by one diff --git a/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/retry_handler.py b/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/retry_handler.py index 28236fc265..d80ed00f88 100644 --- a/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/retry_handler.py +++ b/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/retry_handler.py @@ -1,23 +1,21 @@ -"""ST-10.2 retry-aware emission for LangChain. +"""Retry-aware emission for LangChain. -Per RETRY_LOOP.md §4.4 LangChain row + §4.4.2 coverage limitation + -ST-10.0 C2 empirical verification (POC results in -phase_st10_retryloop.txt): +Design contract: - - Hook BOTH ``on_chat_model_start`` (chat models, F1 finding) + - Hook BOTH ``on_chat_model_start`` (chat models) AND ``on_llm_start`` (legacy completion LLMs). - Per-HTTP-attempt firing is verified on framework-layer retry paths (e.g. ``Runnable.with_retry``); provider-SDK-internal retries (e.g. ``ChatOpenAI(max_retries=N)``) fire callbacks - once per logical call → §4.4.2 coverage limitation applies. + once per logical call, so the framework coverage limitation applies. - Use ``run_id`` as the per-attempt correlation key. - Emit ``fortifyroot.langchain.attempt_`` sibling spans under the parent_run_id's span (NOT under the per-LLM Traceloop span — siblings need to share a parent for RetryDetectorProc's grouping to work). - - §4.7.1: register/unregister framework-attempt tokens so - direct-SDK wrappers (ST-10.4) suppress their own emission. - - §4.5: set has_attempt_child=true on the parent AFTER + - Register/unregister framework-attempt tokens so direct-SDK + wrappers suppress their own emission. + - Set has_attempt_child=true on the parent AFTER the first qualifying llm_attempt successfully starts. - Attempt numbering is conservative for LangChain: callbacks expose a workflow parent that can contain multiple unrelated LLM calls, so @@ -28,7 +26,7 @@ TraceloopCallbackHandler (not as a replacement) — it captures metadata only (model, tokens, status), so its placement is not safety-critical. The existing handler continues to do prompt / -completion attribute capture; the §4.5 backend dedup correctly +completion attribute capture; backend dedup correctly skips its per-attempt spans because they're non-retry siblings of the llm_attempts emitted here. """ @@ -64,18 +62,18 @@ logger = logging.getLogger(__name__) -# ST-10 §4.4: per-attempt sibling span name + role. +# Per-attempt sibling span name + role. _FR_SPAN_ROLE_KEY = "fortifyroot.span.role" _FR_LLM_ATTEMPT_SPAN_NAME_PREFIX = "fortifyroot.langchain" _FR_SPAN_ROLE_LLM_ATTEMPT = "llm_attempt" -# ST-10 §4.5 parent marker. +# Parent marker. _FR_HAS_ATTEMPT_CHILD_KEY = FR_HAS_ATTEMPT_CHILD_KEY # ---------------------------------------------------------------------- # Per-attempt correlation map. Key = LangChain run_id (UUID per attempt -# on framework-layer retry paths — verified ST-10.0 C2 POC). Value = +# on framework-layer retry paths). Value = # {span, started_at_monotonic, framework_token, ended}. Bounded-size + # TTL eviction defends against framework crashes that leave attempts # open. Mirrors the LiteLLM map's shape for consistency across wrappers. @@ -194,7 +192,7 @@ def _resolve_parent_span( retry_attempts must share one OTel parent → RetryDetectorProc can group them. - Resolution strategy (revised 2026-05-11 after review-round-2): + Resolution strategy: Strategy A — TRACELOOP SPANS-DICT LOOKUP (load-bearing when parent_run_id is set): @@ -209,7 +207,7 @@ def _resolve_parent_span( The ``traceloop_handler`` is passed as a DIRECT reference at ``_FortifyRootRetryHandler`` construction time (NOT via a mutable shared back-reference to BaseCallbackManager). This - fixes the review-round-2 Major-5 concern: a single shared + avoids a single shared ``_FortifyRootRetryHandler`` instance was being mutated per-BaseCallbackManager-init, racing concurrent callbacks onto the wrong manager's spans dict. @@ -226,14 +224,14 @@ def _resolve_parent_span( its per-LLM span as the OTel ambient, so ambient is the correct workflow parent. - No-emission policy (review-round-2 Blocker 2): + No-emission policy: If parent_run_id IS set AND traceloop_handler is provided (production wiring) BUT Strategy A's Traceloop lookup fails (parent_run_id not in its spans dict — e.g. evicted, stale), DO NOT fall back to ambient. Ambient at that moment is likely Traceloop's per-LLM span (handler-order is Traceloop-first) and parenting under it would break sibling-grouping. Return - None; the caller skips emission with a debug log. The §4.5 + None; the caller skips emission with a debug log. Backend backend dedup degrades gracefully (no retry_attempt → parent stays canonical → single LLMUsageEvent per call). """ @@ -327,7 +325,7 @@ def _add_prompt_attrs( ) -> None: """Copy LangChain's request content onto the retry_attempt span. - Backend §4.5 makes retry_attempt the canonical LLMUsageEvent span + Backend dedup makes retry_attempt the canonical LLMUsageEvent span when it exists. Safety E2E tests and customers looking up the canonical event therefore still need the same prompt content that Traceloop's normal LLM span carries. The callback receives prompts @@ -381,7 +379,7 @@ def _start_retry_attempt( parent_span = _resolve_parent_span(parent_run_id, traceloop_handler=traceloop_handler) if parent_span is None: # No ambient parent → orphan retry_attempt would have no place - # in the trace tree. Skip emission. The §4.5 backend dedup + # in the trace tree. Skip emission. Backend dedup # degrades gracefully when no retry_attempt exists. logger.debug( "no ambient parent span for langchain retry_attempt; skipping emission " @@ -447,7 +445,7 @@ def _start_retry_attempt( "ended": False, } - # §4.5 marker timing: set AFTER the first qualifying llm_attempt + # Parent-marker timing: set AFTER the first qualifying llm_attempt # has successfully started under this parent. Idempotent — setting # the attribute twice on the same parent is a no-op. try: @@ -537,7 +535,7 @@ class _FortifyRootRetryHandler(BaseCallbackHandler): """LangChain BaseCallbackHandler that emits one fortifyroot.langchain.attempt_ sibling span per LLM-start callback invocation. Hooks BOTH on_chat_model_start (chat models — - F1 finding from ST-10.0 C2 POC) AND on_llm_start (legacy + observed chat-model path) AND on_llm_start (legacy completion LLMs). Key correlation: ``run_id`` (UUID minted per attempt by LangChain). @@ -560,8 +558,7 @@ class _FortifyRootRetryHandler(BaseCallbackHandler): run_inline: bool = True # Set by ``_BaseCallbackManagerInitWrapper`` at construction time - # (NOT mutated per-callback-manager-init — see review-round-2 - # Major-5). Direct reference to the sibling Traceloop handler whose + # (NOT mutated per-callback-manager-init). Direct reference to the sibling Traceloop handler whose # ``spans`` dict we look up by run_id to resolve the workflow # parent for sibling-grouping across multi-attempt retries. See # ``_resolve_parent_span``. diff --git a/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/span_utils.py b/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/span_utils.py index c39a4d256e..8a63e397ad 100644 --- a/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/span_utils.py +++ b/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/span_utils.py @@ -41,7 +41,7 @@ class SpanHolder: start_time: float = field(default_factory=time.perf_counter) request_model: Optional[str] = None streaming_first_token_time: Optional[float] = None - # ST-10 review-round-2 fix (2026-05-11): every context_api.attach() + # Every context_api.attach() # performed for this span — span-context, suppression, metadata # association_properties — appended here in attach order. ``_end_span`` # detaches them in REVERSE order (LIFO) so OTel's context stack is diff --git a/packages/opentelemetry-instrumentation-langchain/tests/conftest.py b/packages/opentelemetry-instrumentation-langchain/tests/conftest.py index e8e793089d..0895b39302 100644 --- a/packages/opentelemetry-instrumentation-langchain/tests/conftest.py +++ b/packages/opentelemetry-instrumentation-langchain/tests/conftest.py @@ -44,8 +44,8 @@ def get_finished_spans(self): @pytest.fixture(scope="session", name="span_exporter") def fixture_span_exporter(): # The upstream/non-FR LangChain tests assert the historical Traceloop - # spans exactly. ST-10 adds FR implementation spans; keep these legacy - # assertions focused while dedicated ST-10 retry tests use a raw exporter. + # spans exactly. retry-loop work adds FR implementation spans; keep these legacy + # assertions focused while dedicated retry-loop work retry tests use a raw exporter. exporter = _LegacyAssertionSpanExporter() yield exporter diff --git a/packages/opentelemetry-instrumentation-langchain/tests/test_retry_attempt_emission.py b/packages/opentelemetry-instrumentation-langchain/tests/test_retry_attempt_emission.py index 8a2cf7ba4d..c2f981d81e 100644 --- a/packages/opentelemetry-instrumentation-langchain/tests/test_retry_attempt_emission.py +++ b/packages/opentelemetry-instrumentation-langchain/tests/test_retry_attempt_emission.py @@ -1,10 +1,10 @@ -"""Tests for ST-10.2 LangChain retry-aware emission. +"""Tests for LangChain retry-aware attempt emission. Covers: - Instrumentor symmetry: _FortifyRootRetryHandler is wired up by LangchainInstrumentor and removed at uninstrument. - on_chat_model_start path (chat models — F1 finding from - ST-10.0 C2 POC). + retry-loop design C2 POC). - on_llm_start path (legacy completion LLMs). - Single-attempt happy path: 1 llm_attempt span, parent has has_attempt_child=true, llm_attempt has gen_ai.system / @@ -49,7 +49,7 @@ def fresh_tracer(): """A fresh TracerProvider + in-memory exporter installed as the GLOBAL tracer provider (so the retry handler's ``trace.get_tracer(...)`` lookups route through it). Same pattern - as the LiteLLM ST-10.1 tests.""" + as the LiteLLM retry-attempt tests.""" exporter = InMemorySpanExporter() provider = TracerProvider() provider.add_span_processor(SimpleSpanProcessor(exporter)) @@ -110,11 +110,11 @@ def test_instrumentor_wires_retry_handler_globally(instrument_legacy): def test_traceloop_handler_registered_before_fr_retry_handler(instrument_legacy): - """REGRESSION GUARD (review-batch-1 v6 fix 2026-05-11): + """Regression guard for LangChain callback ordering. LangChain dispatches callbacks in registration order. The Traceloop handler MUST run BEFORE the FR retry handler, so Traceloop's context-attach/detach discipline (existing, - ST-6-validated behavior) is preserved. + prior LangChain-validated behavior) is preserved. History: an earlier fix attempted the OPPOSITE order (FR retry first) so the OTel ambient at retry_attempt creation @@ -169,7 +169,7 @@ def test_traceloop_handler_registered_before_fr_retry_handler(instrument_legacy) def test_no_leaked_ambient_context_after_simulated_workflow(instrument_legacy): - """REGRESSION GUARD (review-round-2 end-to-end regression, 2026-05-13): + """Regression guard for leaked ambient context after a workflow. The trace-id-leak bug that motivated this guard: a LangChain workflow that exercises the patched BaseCallbackManager.__init__ @@ -250,8 +250,8 @@ def test_no_leaked_ambient_context_after_simulated_workflow(instrument_legacy): f"OTel ambient context leaked after LangChain workflow — " f"a future test in the same pytest session would inherit " f"trace_id={remaining.trace_id:032x}, span_id=" - f"{remaining.span_id:016x}. This is exactly the bug the " - f"review-batch-1 v6 trace-id-shared fix was meant to prevent." + f"{remaining.span_id:016x}. The ambient context must be " + f"fully detached after the simulated workflow." ) @@ -297,7 +297,7 @@ def test_uninstrument_invokes_unwrap_for_callback_manager(): def test_on_chat_model_start_emits_retry_attempt(fresh_tracer): """Chat models fire on_chat_model_start (NOT on_llm_start) — F1 - finding from ST-10.0 C2 POC. The handler MUST handle it.""" + finding from retry-loop design C2 POC. The handler MUST handle it.""" tracer, exporter, _ = fresh_tracer handler = _FortifyRootRetryHandler() diff --git a/packages/opentelemetry-instrumentation-litellm/opentelemetry/instrumentation/litellm/__init__.py b/packages/opentelemetry-instrumentation-litellm/opentelemetry/instrumentation/litellm/__init__.py index ad397de0e2..760448f839 100644 --- a/packages/opentelemetry-instrumentation-litellm/opentelemetry/instrumentation/litellm/__init__.py +++ b/packages/opentelemetry-instrumentation-litellm/opentelemetry/instrumentation/litellm/__init__.py @@ -61,12 +61,12 @@ # ``buildSafetyWrapperDedupeSet`` cannot observe siblings across batches. _FR_HAS_NATIVE_OTEL_CHILD_KEY = "fortifyroot.span.has_native_otel_child" -# ST-10 §4.5: marker on parent set AFTER the first qualifying llm_attempt +# Marker on parent set AFTER the first qualifying llm_attempt # child has started, so the FR backend's LLMUsageExtractor can dedup the # parent + non-attempt siblings cross-batch (mirrors has_native_otel_child). _FR_HAS_ATTEMPT_CHILD_KEY = FR_HAS_ATTEMPT_CHILD_KEY -# ST-10 §4.4: per-attempt sibling span emitted by _FortifyRootRetryEmitter +# Per-attempt sibling span emitted by _FortifyRootRetryEmitter # under the safety_wrapper parent. _FR_LLM_ATTEMPT_SPAN_NAME_PREFIX = "fortifyroot.litellm" _FR_SPAN_ROLE_LLM_ATTEMPT = "llm_attempt" @@ -187,8 +187,7 @@ async def _ensure_completion_safety_applied_async( # isn't installed (the instrumentor's ``instrumentation_dependencies`` check # guards actual use). # -# Discovered end-to-end during ST-10 review-batch-1 re-verification 2026-05-10 -# after local vendoring. The pre-existing _FortifyRootCompletionLogger is +# Discovered end-to-end after local vendoring. The pre-existing _FortifyRootCompletionLogger is # also duck-typed (same latent bug) but its primary safety-masking path is # synchronous inside _finalize_response, so its callback never firing is # masked in production. The retry emitter has no such backup — purely @@ -200,22 +199,22 @@ async def _ensure_completion_safety_applied_async( # ---------------------------------------------------------------------- -# ST-10 §4.4 / §4.3: retry-attempt sibling-span emission via a second +# Retry-attempt sibling-span emission via a second # LiteLLM CustomLogger. # ---------------------------------------------------------------------- # # The emitter opens one ``fortifyroot.litellm.attempt_`` sibling span # per attempt-start callback fired by LiteLLM, and ends it on the matching -# success/failure callback. Per ST-10.0 C1 source-verified findings, this +# success/failure callback. This # fires per-attempt only on the ``completion_with_retries(num_retries=N)`` -# and ``Router(...)`` retry surfaces — see RETRY_LOOP.md §4.4.2 for the -# documented coverage limitation on the ``completion(num_retries=N)`` -# path (where retries delegate to the underlying provider SDK and are +# and ``Router(...)`` retry surfaces. There is a documented coverage +# limitation on the ``completion(num_retries=N)`` path (where retries +# delegate to the underlying provider SDK and are # invisible to LiteLLM's callback layer). -# Per-attempt correlation map (§4.3). Key = LiteLLM's per-call +# Per-attempt correlation map. Key = LiteLLM's per-call # ``litellm_call_id`` (sufficient because each attempt at the -# observable surfaces gets a fresh ID — verified ST-10.0 C2 POC). Value = +# observable surfaces gets a fresh ID). Value = # {span, started_at_monotonic, parent_span, framework_token, ended}. # Bounded-size + TTL eviction defends against framework crashes that # leave attempts open. @@ -327,7 +326,7 @@ def _set_active_retry_attempt_attribute(parent, key: str, value) -> None: def _resolve_routed_provider(kwargs) -> Optional[str]: """Best-effort: derive the ROUTED provider (e.g. ``openai``) from LiteLLM kwargs for the retry_attempt span's ``gen_ai.system`` - attribute. Per RETRY_LOOP.md §4.2, this is the routed provider + attribute. This is the routed provider (NOT the framework name). Falls back to ``litellm`` if undetermined. """ candidate = ( @@ -353,7 +352,7 @@ def _resolve_routed_provider(kwargs) -> Optional[str]: # ``claude-4-sonnet-20250514``) even when the public # call used ``anthropic/``. Without this inference # retry_attempt spans fall back to gen_ai.system="litellm", - # so the backend stores the canonical ST-10 event under the + # so the backend stores the canonical event under the # framework rather than the routed provider. raw = "anthropic" elif model and "." in model: @@ -374,13 +373,13 @@ def _resolve_routed_provider(kwargs) -> Optional[str]: # Normalisation for the ``gen_ai.system`` attribute. # -# Per RETRY_LOOP.md §4.2, the value MUST be the ROUTED provider, NOT +# The value MUST be the ROUTED provider, NOT # the framework, AND it MUST be the canonical OTel-semconv form (e.g. # Bedrock = ``"AWS"``). LiteLLM's ``custom_llm_provider`` field uses # its own taxonomy (``"bedrock"``, ``"bedrock_converse"``, -# ``"sagemaker"``, ...), so we map those to the §4.2 canonical -# values and leave already-canonical values untouched. (Review-batch-1 -# Minor 4 fix 2026-05-10 — keeps cross-wrapper consistency with +# ``"sagemaker"``, ...), so we map those to canonical +# values and leave already-canonical values untouched. This keeps +# cross-wrapper consistency with # LangChain's _resolve_routed_provider which already normalises # langchain_aws → ``"AWS"``.) _LITELLM_PROVIDER_NORMALISATION = { @@ -399,7 +398,7 @@ def _resolve_routed_provider(kwargs) -> Optional[str]: def _normalize_routed_provider(raw: str) -> str: - """Map LiteLLM's provider taxonomy to RETRY_LOOP.md §4.2's + """Map LiteLLM's provider taxonomy to the retry-attempt contract's routed-provider form. If no mapping applies, return the raw value lower-cased (matches OpenAI / Anthropic which already use the canonical form). @@ -452,7 +451,7 @@ def _add_retry_attempt_prompt_attrs( ) -> None: """Copy request prompt content onto the retry_attempt span. - Backend §4.5 makes retry_attempt the canonical LLMUsageEvent span + Backend dedup makes retry_attempt the canonical LLMUsageEvent span when it exists. Safety correlation and masking assertions therefore need the same request content on the retry_attempt span that the safety_wrapper parent carries. @@ -514,7 +513,7 @@ def _start_retry_attempt_span(kwargs, *, is_text_completion: bool = False) -> No attrs[GenAIAttributes.GEN_AI_REQUEST_MODEL] = str(model) server = _server_address(kwargs) if server: - # Per RETRY_LOOP.md §4.2, attribute name is the OTel-standard + # Attribute name is the OTel-standard # ``server.address`` (network namespace) — using the literal # key string for forward-compat across semconv lib changes. attrs["server.address"] = server @@ -533,7 +532,7 @@ def _start_retry_attempt_span(kwargs, *, is_text_completion: bool = False) -> No context=parent_ctx, ) - # §4.7.1: register a framework-attempt token so direct-SDK + # Register a framework-attempt token so direct-SDK # wrappers (OpenAI/Anthropic/Bedrock) suppress their own emission # while this attempt is in flight. try: @@ -557,7 +556,7 @@ def _start_retry_attempt_span(kwargs, *, is_text_completion: bool = False) -> No "ended": False, } - # §4.5 marker timing: set has_attempt_child=true on the + # Parent-marker timing: set has_attempt_child=true on the # parent ONLY AFTER the first qualifying llm_attempt has # successfully started. We just succeeded; mark the parent now. # Idempotent: setting the attribute twice on the same parent is a @@ -615,7 +614,7 @@ def _finalize_retry_attempt_span( output_tokens = get_object_value(usage, "completion_tokens") if output_tokens is None: output_tokens = get_object_value(usage, "output_tokens") - # Per §4.2 token-usage rule: SET when known, OMIT when unknown. + # Token-usage rule: SET when known, OMIT when unknown. if input_tokens is not None: span.set_attribute(GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS, int(input_tokens)) if output_tokens is not None: @@ -625,7 +624,7 @@ def _finalize_retry_attempt_span( exception = kwargs.get("exception") if isinstance(kwargs, dict) else None status_code = _httpx_status_code(exception) or _httpx_status_code(response_obj) if status_code is not None: - # Per RETRY_LOOP.md §4.2 the attribute name is the + # The attribute name is the # OTel-standard ``http.status_code`` (legacy semconv; # backend extractor reads the literal key, not a # python-binding constant). @@ -778,7 +777,7 @@ def _instrument(self, **kwargs): litellm.callbacks = [] self._fr_logger = _FortifyRootCompletionLogger() litellm.callbacks.insert(0, self._fr_logger) - # ST-10.1: register the retry-attempt emitter immediately + # Register the retry-attempt emitter immediately # AFTER the completion logger so completion-safety masking # still runs first. The retry emitter captures metadata # only (model, tokens, status), so its placement is not @@ -873,7 +872,7 @@ def _invoke_completion(tracer, wrapped, args, kwargs, *, is_text_completion=Fals if _native_otel_callback_active(): # Hint to the FR backend that this safety_wrapper WILL have a # sibling litellm_request child emitted by LiteLLM's native OTel - # callback — enables single-pass dedup in proc_llm_extractor.go + # callback — enables single-pass dedup in backend LLM extractor # without needing to see the child in the same OTLP batch. span_attrs[_FR_HAS_NATIVE_OTEL_CHILD_KEY] = True span = tracer.start_span( diff --git a/packages/opentelemetry-instrumentation-litellm/tests/test_logger_integration.py b/packages/opentelemetry-instrumentation-litellm/tests/test_logger_integration.py index 4ae0281bbe..9397e95d2d 100644 --- a/packages/opentelemetry-instrumentation-litellm/tests/test_logger_integration.py +++ b/packages/opentelemetry-instrumentation-litellm/tests/test_logger_integration.py @@ -264,7 +264,7 @@ def test_logger_non_text_content_is_noop(): # --------------------------------------------------------------------------- def test_instrumentor_registers_logger_at_position_zero(): - """_FortifyRootCompletionLogger must be at index 0, and (post-ST-10.1) + """_FortifyRootCompletionLogger must be at index 0, and (post-LiteLLM retry-attempt) _FortifyRootRetryEmitter must be at index 1, with any pre-existing customer callbacks pushed to index 2+ after instrument().""" import litellm diff --git a/packages/opentelemetry-instrumentation-litellm/tests/test_retry_attempt_emission.py b/packages/opentelemetry-instrumentation-litellm/tests/test_retry_attempt_emission.py index a07f6a8243..03a180c0b6 100644 --- a/packages/opentelemetry-instrumentation-litellm/tests/test_retry_attempt_emission.py +++ b/packages/opentelemetry-instrumentation-litellm/tests/test_retry_attempt_emission.py @@ -1,9 +1,9 @@ -"""Tests for ST-10.1 LiteLLM retry-aware emission. +"""Tests for LiteLLM retry-aware attempt emission. Covers: - Instrumentor symmetry: _FortifyRootRetryEmitter is registered at instrument() AND removed at uninstrument() (Fallback B child-emission - proof requirement (ii) per RETRY_LOOP.md §4.4.1). + proof requirement (ii) per retry-loop design notes §4.4.1). - Single-attempt happy path: one llm_attempt span emitted under the safety_wrapper parent, parent carries has_attempt_child=true. - Multi-attempt retry path: 3 retry_attempt spans emitted (2 ERROR + 1 @@ -127,14 +127,14 @@ def _assert_attempt_sequence(spans): # --------------------------------------------------------------------------- def test_retry_emitter_inherits_from_litellm_custom_logger(): - """REGRESSION GUARD (review-batch-1 end-to-end discovery 2026-05-10): + """Regression guard for LiteLLM callback dispatch compatibility. LiteLLM's dispatch loop gates every callback hook on ``isinstance(callback, CustomLogger)`` — see litellm_logging.py:1015 (log_pre_api_call) and :2303 (log_success_event). A duck-typed _FortifyRootRetryEmitter is silently SKIPPED by the dispatch — log_pre_api_call never fires → no retry_attempt span ever emitted → §4.5 backend dedup has - nothing to dedup → the entire ST-10.1 contract is no-op'd. + nothing to dedup → the entire LiteLLM retry-attempt contract is no-op'd. The bug was invisible to fork-side unit tests (which call the emitter's hooks directly, bypassing LiteLLM's dispatch). End-to-end @@ -340,7 +340,7 @@ def test_three_attempt_retry_path_emits_three_retry_attempt_spans(fresh_tracer): LiteLLM retry helpers re-enter completion() per attempt and therefore create one safety_wrapper per attempt; those multi-trace paths can correctly surface as separate attempt_1 spans, as documented in - RETRY_LOOP.md. + retry-loop design notes. """ tracer, exporter, _ = fresh_tracer @@ -547,15 +547,15 @@ def test_no_parent_span_does_not_emit_orphan_retry_attempt(fresh_tracer): def test_resolve_routed_provider_normalisation(): - """REGRESSION GUARD (review-batch-1 Minor 4 fix 2026-05-10): + """Regression guard for routed-provider normalization. LiteLLM's ``custom_llm_provider`` taxonomy uses values like ``"bedrock"``, ``"bedrock_converse"``, ``"vertex_ai"`` that - diverge from RETRY_LOOP.md §4.2's canonical routed-provider + diverge from retry-loop design notes §4.2's canonical routed-provider form (``"AWS"``, ``"google"``, etc.). Cross-wrapper drift here would mean LiteLLM-routed-Bedrock spans carry ``gen_ai.system="bedrock"`` while LangChain/LlamaIndex Bedrock spans carry ``gen_ai.system="AWS"``, breaking the - ST-10.0 §4.2 cross-wrapper consistency contract. + retry-loop design §4.2 cross-wrapper consistency contract. """ # AWS Bedrock — all variants must normalise to "AWS". assert _resolve_routed_provider({"custom_llm_provider": "bedrock"}) == "AWS" diff --git a/packages/opentelemetry-instrumentation-llamaindex/opentelemetry/instrumentation/llamaindex/dispatcher_wrapper.py b/packages/opentelemetry-instrumentation-llamaindex/opentelemetry/instrumentation/llamaindex/dispatcher_wrapper.py index 4d0ec24146..4d1de8bb67 100644 --- a/packages/opentelemetry-instrumentation-llamaindex/opentelemetry/instrumentation/llamaindex/dispatcher_wrapper.py +++ b/packages/opentelemetry-instrumentation-llamaindex/opentelemetry/instrumentation/llamaindex/dispatcher_wrapper.py @@ -156,7 +156,7 @@ def _stamp_llm_response_model_for_safety(event: BaseEvent, span) -> None: def instrument_with_dispatcher(tracer: Tracer): instrument_llm_safety_wrappers() dispatcher = get_dispatcher() - # ST-10.3: register the FR retry-attempt handler FIRST, before + # Register the FR retry-attempt handler FIRST, before # OpenLLMetrySpanHandler. Order matters because span handlers # fire in registration order — and our handler reads the # ambient OTel context to decide the parent of the @@ -383,7 +383,7 @@ def new_span( # ``_stamp_llm_model_for_safety()`` to keep safety findings # emitted on this span (via emit_deferred_findings) correctly # attributed to model / provider. See - # fr-system-tests st_phase_6.txt addendum 8. + # framework safety attribution regression coverage. if is_openllmetry_class: span.set_attribute(_FR_LLM_WRAPPER_ROLE_KEY, _FR_LLM_WRAPPER_ROLE_VALUE) try: diff --git a/packages/opentelemetry-instrumentation-llamaindex/opentelemetry/instrumentation/llamaindex/retry_handler.py b/packages/opentelemetry-instrumentation-llamaindex/opentelemetry/instrumentation/llamaindex/retry_handler.py index 4682260c87..a2bbd2e101 100644 --- a/packages/opentelemetry-instrumentation-llamaindex/opentelemetry/instrumentation/llamaindex/retry_handler.py +++ b/packages/opentelemetry-instrumentation-llamaindex/opentelemetry/instrumentation/llamaindex/retry_handler.py @@ -1,21 +1,19 @@ -"""ST-10.3 retry-aware emission for LlamaIndex. +"""Retry-aware emission for LlamaIndex. -Per RETRY_LOOP.md §4.4 LlamaIndex row + §4.4.2 coverage limitation + -ST-10.0 C3 empirical verification (POC findings F4-F6 in -phase_st10_retryloop.txt): +Design contract: - LlamaIndex's ``OpenAI.chat()`` (and equivalents) fires the dispatcher span TWICE per HTTP attempt — once on the public ``chat()`` method and once on the inner ``_chat()`` (F4). A naive event-handler approach emits 2x retry_attempt children - per attempt. ST-10.3 hooks the SPAN HANDLER (not the event + per attempt. This hooks the SPAN HANDLER (not the event handler) and filters to OUTER public methods only via a method-name whitelist + ``BaseLLM`` instance check. - Per-attempt firing is verified empirically on framework-layer retry paths (e.g. tenacity wrapping at the application layer). Provider-SDK-internal retries (e.g. ``OpenAI(max_retries=N).chat(...)``) fire dispatcher spans ONCE - per logical call → §4.4.2 coverage limitation applies (same + per logical call, so the framework coverage limitation applies (same pattern as LiteLLM C1 / LangChain C2). - ``span_enter``/``span_exit``/``span_drop`` lifecycle hooks correlate cleanly: each enter has a matching exit OR drop, so @@ -58,12 +56,12 @@ logger = logging.getLogger(__name__) -# ST-10 §4.4: per-attempt sibling span name + role. +# Per-attempt sibling span name + role. _FR_SPAN_ROLE_KEY = "fortifyroot.span.role" _FR_LLM_ATTEMPT_SPAN_NAME_PREFIX = "fortifyroot.llamaindex" _FR_SPAN_ROLE_LLM_ATTEMPT = "llm_attempt" -# ST-10 §4.5 parent marker. +# Parent marker. _FR_HAS_ATTEMPT_CHILD_KEY = FR_HAS_ATTEMPT_CHILD_KEY # Dispatcher span IDs follow the pattern "ClassName.method-uuid". @@ -217,7 +215,7 @@ def _content_to_string(content: Any) -> str: def _add_prompt_attrs(attrs: dict[str, Any], bound_args: Any) -> None: """Copy request prompt content onto the retry_attempt span. - Backend §4.5 makes retry_attempt the canonical LLMUsageEvent span + Backend dedup makes retry_attempt the canonical LLMUsageEvent span when it exists, so safety correlation still needs prompt content on this span. LlamaIndex safety wrappers have already processed the bound arguments by the time dispatcher span handlers see them. @@ -326,7 +324,7 @@ def _start_retry_attempt(id_: str, instance: Any, bound_args: Any = None) -> Non "ended": False, } - # §4.5 marker timing: set on parent AFTER the first qualifying + # Parent-marker timing: set on parent AFTER the first qualifying # llm_attempt has successfully started under it. try: parent_span.set_attribute(_FR_HAS_ATTEMPT_CHILD_KEY, True) @@ -407,8 +405,7 @@ class _FortifyRootRetryHandler(BaseSpanHandler): public LLM method invocation (chat/achat/complete/acomplete/...). De-dup vs the inner ``_chat``/``_complete`` spans is enforced by - a method-name whitelist in ``_is_outer_llm_method`` (F4 finding - from ST-10.0 C3 POC). So one HTTP attempt = exactly one + a method-name whitelist in ``_is_outer_llm_method``. So one HTTP attempt = exactly one retry_attempt span — even though LlamaIndex internally fires dispatcher spans on both the outer and inner methods. diff --git a/packages/opentelemetry-instrumentation-llamaindex/opentelemetry/instrumentation/llamaindex/safety.py b/packages/opentelemetry-instrumentation-llamaindex/opentelemetry/instrumentation/llamaindex/safety.py index c0b2b6d6b2..3a10011f28 100644 --- a/packages/opentelemetry-instrumentation-llamaindex/opentelemetry/instrumentation/llamaindex/safety.py +++ b/packages/opentelemetry-instrumentation-llamaindex/opentelemetry/instrumentation/llamaindex/safety.py @@ -51,8 +51,8 @@ def _infer_llm_provider(model) -> str | None: model_name = str(model or "").lower() - # TODO(ST-6 follow-up): MVP support here is intentionally limited to the - # providers exercised in ST-6 (OpenAI + Anthropic). Expand this inference + # TODO: support here is intentionally limited to the currently + # certified providers (OpenAI + Anthropic). Expand this inference # when we certify more LlamaIndex-backed providers so safety-emitted # wrapper spans continue to carry provider/model attribution for them. if "claude" in model_name or "anthropic" in model_name: diff --git a/packages/opentelemetry-instrumentation-llamaindex/opentelemetry/instrumentation/llamaindex/span_utils.py b/packages/opentelemetry-instrumentation-llamaindex/opentelemetry/instrumentation/llamaindex/span_utils.py index 0092ab59b2..feee0e9104 100644 --- a/packages/opentelemetry-instrumentation-llamaindex/opentelemetry/instrumentation/llamaindex/span_utils.py +++ b/packages/opentelemetry-instrumentation-llamaindex/opentelemetry/instrumentation/llamaindex/span_utils.py @@ -18,8 +18,8 @@ def _infer_llm_provider(model) -> str | None: model_name = str(model or "").lower() - # TODO(ST-6 follow-up): MVP support here is intentionally limited to the - # providers exercised in ST-6 (OpenAI + Anthropic). Expand this inference + # TODO: support here is intentionally limited to the currently + # certified providers (OpenAI + Anthropic). Expand this inference # as additional LlamaIndex-backed providers are certified so delegated # wrapper spans and backend LLMUsage attribution keep working for them too. if "claude" in model_name or "anthropic" in model_name: @@ -114,7 +114,7 @@ def set_llm_chat_response_model_attributes(event, span): output_tokens = None total_tokens = None - # TODO(ST-6 follow-up): token extraction below currently covers the usage + # TODO: token extraction below currently covers the usage # shapes we needed for the MVP providers/certified paths (OpenAI-style, # Anthropic-style, and Cohere metadata fallback). Extend this branch as # additional LlamaIndex-backed providers are added so token attribution diff --git a/packages/opentelemetry-instrumentation-llamaindex/tests/test_retry_attempt_emission.py b/packages/opentelemetry-instrumentation-llamaindex/tests/test_retry_attempt_emission.py index 0e37d1d591..4531de84a3 100644 --- a/packages/opentelemetry-instrumentation-llamaindex/tests/test_retry_attempt_emission.py +++ b/packages/opentelemetry-instrumentation-llamaindex/tests/test_retry_attempt_emission.py @@ -1,4 +1,4 @@ -"""Tests for ST-10.3 LlamaIndex retry-aware emission. +"""Tests for LlamaIndex retry-aware attempt emission. Covers: - F4 de-dup contract: outer chat() AND inner _chat() both fire @@ -49,7 +49,7 @@ @pytest.fixture def fresh_tracer(): """Fresh TracerProvider + in-memory exporter installed as global, - matching the ST-10.1 / ST-10.2 fixture pattern.""" + matching the LiteLLM retry-attempt / LangChain retry-attempt fixture pattern.""" exporter = InMemorySpanExporter() provider = TracerProvider() provider.add_span_processor(SimpleSpanProcessor(exporter)) @@ -170,7 +170,7 @@ class _Usage: def test_outer_method_emits_inner_method_does_not(fresh_tracer): """LlamaIndex dispatcher fires spans on BOTH the public ``chat()`` - AND the inner ``_chat()`` (F4 finding from POC). ST-10.3 hooks + AND the inner ``_chat()`` (F4 finding from POC). LlamaIndex retry-attempt hooks SpanHandler.new_span and filters via ``_is_outer_llm_method`` → only the outer method emits a retry_attempt span. diff --git a/packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/retry_handler.py b/packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/retry_handler.py index a3e11f115f..2ede36d273 100644 --- a/packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/retry_handler.py +++ b/packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/retry_handler.py @@ -1,7 +1,6 @@ -"""ST-10.4 retry-aware emission for the OpenAI direct SDK. +"""Retry-aware emission for the OpenAI direct SDK. -Per RETRY_LOOP.md §4.4 OpenAI row + §4.7 suppression discipline + -ST-10.0 hook-table addendum (in phase_st10_retryloop.txt): +Design contract: - Hook the SDK-internal private httpx wrapper classes ``openai._base_client.SyncHttpxClientWrapper.send`` and @@ -24,9 +23,8 @@ (``/v1/chat/completions``, ``/v1/completions``, ``/v1/embeddings``, ``/v1/responses``, plus Azure ``/openai/deployments/.../chat/completions``). Non-LLM SDK traffic (e.g. ``/v1/models`` for token-refresh / model - listing) does NOT emit retry_attempt — per §4.4.1 allow-listing - requirement. - - Suppression discipline (§4.7): before emitting, check BOTH the + listing) does NOT emit retry_attempt. + - Suppression discipline: before emitting, check BOTH the OTel context ``SUPPRESS_LANGUAGE_MODEL_INSTRUMENTATION_KEY`` AND the shared ``is_framework_owned()`` registry. If either says "suppress", we skip emission. This is the key invariant that @@ -34,8 +32,8 @@ DOUBLE-emitting when both framework wrappers and direct-SDK wrappers are active. - Parent resolution: use the current OTel ambient span. If invalid - or absent, gracefully skip (no orphan retry_attempt). The §4.5 - backend dedup degrades gracefully. + or absent, gracefully skip (no orphan retry_attempt). Backend + dedup degrades gracefully. - Per-attempt span lifetime: open on wrap entry, end on wrap exit (success OR exception). On non-2xx HTTP, set ``http.status_code`` + ``error.type`` and ERROR status; on 2xx, @@ -71,7 +69,7 @@ logger = logging.getLogger(__name__) -# ST-10 §4.4 / §4.5 constants. +# Retry-attempt constants. _FR_SPAN_ROLE_KEY = "fortifyroot.span.role" _FR_LLM_ATTEMPT_SPAN_NAME_PREFIX = "fortifyroot.openai" _FR_SPAN_ROLE_LLM_ATTEMPT = "llm_attempt" @@ -227,7 +225,7 @@ def _server_attrs_from_request(request: Any) -> dict[str, Any]: def _is_suppressed() -> bool: - """§4.7: skip emission if EITHER suppression signal is active. + """Skip emission if EITHER suppression signal is active. Subtle: the OpenAI ``chat_wrapper`` (and ``achat_wrapper``) sets ``SUPPRESS_LANGUAGE_MODEL_INSTRUMENTATION_KEY`` around its own @@ -270,7 +268,7 @@ def _resolve_parent_span() -> Optional["trace.Span"]: def _set_parent_marker(parent_span: "trace.Span") -> None: - """§4.5: mark the parent as 'has llm_attempt child' AFTER the + """Mark the parent as 'has llm_attempt child' AFTER the first child has successfully started. Idempotent — setting twice is a no-op.""" try: @@ -317,15 +315,15 @@ def _finalize_success(span: "trace.Span", response: Any, *, is_streaming: bool = For non-streaming responses (regardless of 2xx vs non-2xx), parse the JSON body and extract usage tokens + response id + response - model. Per RETRY_LOOP.md §4.4 token-usage rule (around line 164): + model. Per the retry-attempt token-usage rule: wrappers MUST extract usage from the response body whenever it's present, regardless of whether the attempt succeeded — some failures (context-length-exceeded errors etc.) DO consume tokens - and the provider returns usage in the error body. Backend §4.5 + and the provider returns usage in the error body. Backend dedup makes a qualifying retry_attempt canonical (even single- attempt) and reads token attrs from it - (``fr-backend/internal/processing/proc_llm_extractor.go`` - ``isRetryAttempt`` block) — so usage attribution must live here. + retry-attempt dedup reads token attrs from retry_attempt spans, so + usage attribution must live here. For streaming responses (``stream=True`` passed to ``send()``), body reading would consume the SSE stream before the SDK can @@ -351,7 +349,7 @@ def _finalize_success(span: "trace.Span", response: Any, *, is_streaming: bool = pass # Non-streaming body parse — applies to BOTH 2xx and non-2xx per the - # §4.4 token-usage rule. ``_extract_usage_from_body`` is fully + # retry-attempt token-usage rule. ``_extract_usage_from_body`` is fully # defensive: missing fields, parse failure, and bodies without # ``usage`` all degrade silently to "no attrs set". if not is_streaming: @@ -475,31 +473,31 @@ def _sync_send_wrapper(wrapped, instance, args, kwargs): request. Framework-attempt registry: this wrapper does NOT register a token. - The §4.7.1 registry's documented contract (see ``retry_registry.py`` + The framework-attempt registry's documented contract (see ``retry_registry.py`` docstring) reserves registration for FRAMEWORK wrappers (LiteLLM / LangChain / LlamaIndex). Direct-SDK wrappers only CONSULT via ``is_framework_owned()``. Self-registering would falsely suppress other concurrent direct-SDK calls on the same OS thread — a real issue under asyncio where two tasks share a thread. - Streaming skip (ST-10.4 review-driven 2026-05-17): when + Streaming skip: when ``stream=True`` is passed to send, this wrapper SKIPS retry_attempt emission entirely. Rationale: streaming retry_attempts cannot carry token usage (the body is the SSE stream and reading it would - break the SDK), but backend §4.5 dedup makes any qualifying + break the SDK), but backend dedup makes any qualifying retry_attempt the canonical LLMUsageEvent — producing a zero-token canonical event for streaming calls. By not emitting at all, the parent ``openai.chat`` span (which DOES get full usage attribution from ``ChatStream``'s stream-completion callback) remains the canonical event. Streaming retry-loop detection is the documented - deferred follow-up ``ST-10.4-FOLLOWUP-streaming-usage``; this - explicit skip is part of that deferral. + deferred streaming-usage follow-up; this explicit skip is part of + that deferral. """ request = args[0] if args else kwargs.get("request") if request is None or not _should_emit_for(request): return wrapped(*args, **kwargs) - # ST-10.4: skip retry_attempt emission for streaming calls. + # Skip retry_attempt emission for streaming calls. if bool(kwargs.get("stream", False)): return wrapped(*args, **kwargs) @@ -542,7 +540,7 @@ async def _async_send_wrapper(wrapped, instance, args, kwargs): if request is None or not _should_emit_for(request): return await wrapped(*args, **kwargs) - # ST-10.4: skip retry_attempt emission for streaming (see + # Skip retry_attempt emission for streaming (see # _sync_send_wrapper docstring for rationale). if bool(kwargs.get("stream", False)): return await wrapped(*args, **kwargs) @@ -632,7 +630,7 @@ def instrument_retry_emitter(tracer_provider=None) -> None: ) if not sync_ok: logger.warning( - "ST-10.4: openai._base_client.%s.%s missing/incompatible; " + "OpenAI retry emitter: openai._base_client.%s.%s missing/incompatible; " "skipping sync retry_attempt emission. Normal openai " "instrumentation is unaffected.", _SYNC_WRAPPER_CLASS, _WRAPPED_METHOD, @@ -646,13 +644,13 @@ def instrument_retry_emitter(tracer_provider=None) -> None: ) except Exception as e: logger.warning( - "ST-10.4: failed to wrap openai sync httpx send (%s); " + "OpenAI retry emitter: failed to wrap openai sync httpx send (%s); " "retry_attempt emission disabled for sync path", e, ) if not async_ok: logger.warning( - "ST-10.4: openai._base_client.%s.%s missing/incompatible; " + "OpenAI retry emitter: openai._base_client.%s.%s missing/incompatible; " "skipping async retry_attempt emission. Normal openai " "instrumentation is unaffected.", _ASYNC_WRAPPER_CLASS, _WRAPPED_METHOD, @@ -666,7 +664,7 @@ def instrument_retry_emitter(tracer_provider=None) -> None: ) except Exception as e: logger.warning( - "ST-10.4: failed to wrap openai async httpx send (%s); " + "OpenAI retry emitter: failed to wrap openai async httpx send (%s); " "retry_attempt emission disabled for async path", e, ) @@ -687,7 +685,7 @@ def uninstrument_retry_emitter() -> None: unwrap(f"{_OPENAI_BASE_CLIENT_MODULE}.{cls}", _WRAPPED_METHOD) except Exception: logger.debug( - "ST-10.4: openai unwrap of %s.%s failed (likely " + "OpenAI retry emitter: unwrap of %s.%s failed (likely " "wrap was never installed for this variant)", cls, _WRAPPED_METHOD, exc_info=True, diff --git a/packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/chat_wrappers.py b/packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/chat_wrappers.py index ef0274e881..5c1d58a015 100644 --- a/packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/chat_wrappers.py +++ b/packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/chat_wrappers.py @@ -78,8 +78,7 @@ # FR: stable internal span attributes carrying streaming latency as integer # milliseconds, so the backend can extract TTFT/STTG into RDS llm_usage_events. -# Additive to (not a replacement for) the Mimir streaming histograms. Contract: -# fr-backend/docs/development/STREAMING_LATENCY_TTFT_STTG_PLAN.md +# Additive to (not a replacement for) the Mimir streaming histograms. FR_STREAMING_TIME_TO_FIRST_TOKEN_MS = "fortifyroot.llm.streaming.time_to_first_token_ms" FR_STREAMING_TIME_TO_GENERATE_MS = "fortifyroot.llm.streaming.time_to_generate_ms" @@ -127,7 +126,7 @@ def chat_wrapper( run_async(_handle_request(span, kwargs, instance)) try: start_time = time.perf_counter() - # ST-10.4 (review-driven 2026-05-16): set + # Set # OPENAI_DIRECT_RETRY_PARENT_ACTIVE_KEY alongside the # existing SUPPRESS_LANGUAGE_MODEL_INSTRUMENTATION_KEY so # the FortifyRoot retry handler can distinguish this @@ -248,7 +247,7 @@ async def achat_wrapper( try: start_time = time.perf_counter() - # ST-10.4 (review-driven 2026-05-16): see sync chat_wrapper + # See sync chat_wrapper # above for the rationale on OPENAI_DIRECT_RETRY_PARENT_ACTIVE_KEY. attempt_ctx = context_api.set_value( SUPPRESS_LANGUAGE_MODEL_INSTRUMENTATION_KEY, True, diff --git a/packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/v1/__init__.py b/packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/v1/__init__.py index d6fab7ec3c..cafe354131 100644 --- a/packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/v1/__init__.py +++ b/packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/v1/__init__.py @@ -358,7 +358,7 @@ def _instrument(self, **kwargs): realtime_connect_wrapper(tracer), ) - # ST-10.4: per-attempt retry_attempt emission via private + # Per-attempt retry_attempt emission via private # ``openai._base_client`` httpx wrapper classes. Guarded against # missing private symbols (logs warning + skips emission). Pass # the same tracer_provider the rest of the instrumentor uses so @@ -370,7 +370,7 @@ def _instrument(self, **kwargs): instrument_retry_emitter(tracer_provider=tracer_provider) def _uninstrument(self, **kwargs): - uninstrument_retry_emitter() # ST-10.4 symmetry + uninstrument_retry_emitter() # retry-emitter symmetry unwrap_dotted_method("openai.resources.chat.completions", "Completions.create") unwrap_dotted_method("openai.resources.completions", "Completions.create") unwrap_dotted_method("openai.resources.embeddings", "Embeddings.create") diff --git a/packages/opentelemetry-instrumentation-openai/tests/conftest.py b/packages/opentelemetry-instrumentation-openai/tests/conftest.py index f2ff4611ab..11d09bcb32 100644 --- a/packages/opentelemetry-instrumentation-openai/tests/conftest.py +++ b/packages/opentelemetry-instrumentation-openai/tests/conftest.py @@ -32,10 +32,10 @@ class _NoFortifyRootSpanExporter(InMemorySpanExporter): - """ST-10.4: filter out any span whose name starts with + """FortifyRoot retry-attempt: filter out any span whose name starts with ``fortifyroot.`` from the upstream-test span exporter. - ST-10.4 added per-attempt sibling spans (e.g. + FortifyRoot retry-attempt added per-attempt sibling spans (e.g. ``fortifyroot.openai.attempt_1``) under every openai logical call. Upstream/legacy OpenAI tests assert exact span-name lists (e.g. ``[span.name for span in spans] == ["openai.chat"]``); without @@ -43,7 +43,7 @@ class _NoFortifyRootSpanExporter(InMemorySpanExporter): retry_attempt sibling is also exported. Mirrors the same pattern used by the LangChain test conftest from - the 2026-05-15 CI-hardening addendum. ST-10.4 unit tests in + the 2026-05-15 CI-hardening addendum. FortifyRoot retry-attempt unit tests in ``tests/test_retry_attempt_emission.py`` use their own ``fresh_tracer`` fixture (not this one), so they continue to see retry_attempt spans and aren't affected by the filter. @@ -53,7 +53,7 @@ def get_finished_spans(self): # type: ignore[override] # Filter by role rather than name prefix so legitimate # fortifyroot.*.safety / .llm_wrapper / .has_native_otel_child # spans remain visible to tests that inspect them. Only - # ST-10.4 retry_attempt siblings carry role=retry_attempt. + # FortifyRoot retry-attempt retry_attempt siblings carry role=retry_attempt. return tuple( s for s in super().get_finished_spans() if (s.attributes or {}).get("fortifyroot.span.role") != "llm_attempt" diff --git a/packages/opentelemetry-instrumentation-openai/tests/test_retry_attempt_emission.py b/packages/opentelemetry-instrumentation-openai/tests/test_retry_attempt_emission.py index fb578afc2d..5b3cbff22d 100644 --- a/packages/opentelemetry-instrumentation-openai/tests/test_retry_attempt_emission.py +++ b/packages/opentelemetry-instrumentation-openai/tests/test_retry_attempt_emission.py @@ -1,6 +1,6 @@ -"""Tests for ST-10.4 OpenAI direct-SDK retry-attempt emission. +"""Tests for FortifyRoot retry-attempt OpenAI direct-SDK retry-attempt emission. -Covers (per RETRY_LOOP.md §4.4 OpenAI row + §4.7 suppression): +Covers (per retry-loop design notes §4.4 OpenAI row + §4.7 suppression): - Instrumentor symmetry: install/uninstall flips state cleanly and is idempotent. - Single-attempt happy path: ONE llm_attempt span under the active @@ -520,7 +520,7 @@ def test_framework_registry_suppression_skips_emission(fresh_tracer): # --------------------------------------------------------------------------- def test_direct_sdk_wrapper_does_not_self_register_in_framework_registry(fresh_tracer): - """REGRESSION GUARD (review-driven fix 2026-05-13): direct-SDK + """REGRESSION GUARD: direct-SDK wrappers MUST NOT register tokens in the §4.7.1 framework registry. The registry's contract reserves registration for FRAMEWORK wrappers (LiteLLM / LangChain / LlamaIndex); direct-SDK wrappers only CONSULT @@ -556,8 +556,8 @@ def wrapped(*a, **kw): def test_two_concurrent_async_sends_each_emit_a_retry_attempt(fresh_tracer): - """REGRESSION GUARD (review-driven fix 2026-05-13): two asyncio - tasks sharing one OS thread MUST each emit their own retry_attempt + """REGRESSION GUARD: two asyncio tasks sharing one OS thread + MUST each emit their own retry_attempt span. Pre-fix, the first task's self-registered framework token would suppress the second task via the thread-keyed registry — silently dropping one of the spans.""" @@ -643,7 +643,7 @@ def test_llm_endpoints_emit_with_correct_operation(fresh_tracer, path, expected_ # --------------------------------------------------------------------------- -# §4.5-driven usage-extraction policy (review-driven fix 2026-05-13). +# §4.5-driven usage-extraction policy. # --------------------------------------------------------------------------- def test_non_streaming_response_body_populates_usage_id_and_model(fresh_tracer): @@ -680,19 +680,19 @@ def test_non_streaming_response_body_populates_usage_id_and_model(fresh_tracer): def test_streaming_request_skips_emission_entirely(fresh_tracer): - """ST-10.4 (review-driven 2026-05-17): when ``stream=True`` is + """FortifyRoot retry-attempt: when ``stream=True`` is passed to send, the wrap SKIPS retry_attempt emission entirely — no span is created, and ``response.json()`` is never called. Rationale: streaming retry_attempts cannot carry token usage at attempt-end (SSE stream consumption would break the SDK), but backend §4.5 dedup would promote them to canonical - LLMUsageEvents → zero-token events → fr-system-tests + LLMUsageEvents → zero-token events → system tests ``prompt_tokens > 0`` assertions fail. The parent ``openai.chat`` span (which gets full usage from ``ChatStream``'s stream-completion callback) stays canonical. Streaming retry-loop detection is the deferred follow-up - ``ST-10.4-FOLLOWUP-streaming-usage``. + ``streaming retry-usage follow-up``. """ tracer, exporter, _ = fresh_tracer request = _make_request(model="gpt-4o-mini") @@ -736,8 +736,8 @@ def explode(): def test_non_2xx_response_with_usage_in_body_populates_usage_tokens(fresh_tracer): - """REGRESSION GUARD (review-driven follow-up 2026-05-13): per - RETRY_LOOP.md §4.4 token-usage rule (around line 164), wrappers + """REGRESSION GUARD: per + retry-loop design notes §4.4 token-usage rule (around line 164), wrappers MUST extract usage from the response body whenever it's present, REGARDLESS of whether the attempt succeeded. Some failures (e.g. context-length-exceeded errors) consume tokens and the provider @@ -840,7 +840,7 @@ def explode(): # --------------------------------------------------------------------------- def test_external_suppression_with_no_override_skips_emission(fresh_tracer): - """REGRESSION GUARD (review-driven 2026-05-16, Issue 3B counter-proof): + """REGRESSION GUARD for nested context propagation: when SUPPRESS_LANGUAGE_MODEL_INSTRUMENTATION_KEY is set in the OTel context WITHOUT the OpenAI override key, the retry handler MUST suppress emission. This is the user-explicit "disable LLM @@ -866,7 +866,7 @@ def test_external_suppression_with_no_override_skips_emission(fresh_tracer): def test_openai_wrapper_override_key_unblocks_emission_under_suppression(fresh_tracer): - """REGRESSION GUARD (review-driven 2026-05-16, Issue 3B): + """REGRESSION GUARD for nested context propagation: the openai chat_wrapper sets SUPPRESS_LANGUAGE_MODEL_INSTRUMENTATION_KEY around its wrapped SDK call (to protect against OTHER LLM instrumentors double-counting). To preserve retry_attempt emission @@ -896,7 +896,7 @@ def test_openai_wrapper_override_key_unblocks_emission_under_suppression(fresh_t def test_tracer_provider_plumbed_through_instrument_retry_emitter(): - """REGRESSION GUARD (review-driven 2026-05-16, Issue 3A): + """REGRESSION GUARD for context suppression: ``instrument_retry_emitter(tracer_provider=provider)`` MUST cause the wrapper to emit retry_attempt spans through that explicit provider, not the global default. Without this plumbing, a diff --git a/packages/opentelemetry-instrumentation-openai/tests/test_streaming_latency_span_attrs.py b/packages/opentelemetry-instrumentation-openai/tests/test_streaming_latency_span_attrs.py index 2ce32d379d..4796f15e65 100644 --- a/packages/opentelemetry-instrumentation-openai/tests/test_streaming_latency_span_attrs.py +++ b/packages/opentelemetry-instrumentation-openai/tests/test_streaming_latency_span_attrs.py @@ -11,7 +11,7 @@ # generator directly with fake chunks + stub histograms, so it needs NO OpenAI # API key and NO VCR cassette and is fully deterministic. # -# Contract: fr-backend/docs/development/STREAMING_LATENCY_TTFT_STTG_PLAN.md +# Contract: streaming latency contract import time from opentelemetry.sdk.trace import TracerProvider diff --git a/packages/opentelemetry-instrumentation-openai/tests/traces/test_chat.py b/packages/opentelemetry-instrumentation-openai/tests/traces/test_chat.py index ef7e8b72dc..55c95f1068 100644 --- a/packages/opentelemetry-instrumentation-openai/tests/traces/test_chat.py +++ b/packages/opentelemetry-instrumentation-openai/tests/traces/test_chat.py @@ -7,7 +7,7 @@ from unittest.mock import patch import httpx -from openai import _base_client # ST-10.4: spy target for retry-aware httpx wrap +from openai import _base_client # FortifyRoot retry-attempt: spy target for retry-aware httpx wrap import pytest from openai.resources.chat.completions import Completions from openai.types.chat.chat_completion_message_tool_call import ( diff --git a/packages/opentelemetry-instrumentation-openai/tests/traces/test_completions.py b/packages/opentelemetry-instrumentation-openai/tests/traces/test_completions.py index 67ae15a7e2..3f3bda0959 100644 --- a/packages/opentelemetry-instrumentation-openai/tests/traces/test_completions.py +++ b/packages/opentelemetry-instrumentation-openai/tests/traces/test_completions.py @@ -6,7 +6,7 @@ import httpx import pytest -from openai import _base_client # ST-10.4: spy target for retry-aware httpx wrap +from openai import _base_client # FortifyRoot retry-attempt: spy target for retry-aware httpx wrap from opentelemetry.sdk._logs import ReadableLogRecord from opentelemetry.semconv._incubating.attributes import ( gen_ai_attributes as GenAIAttributes, diff --git a/packages/opentelemetry-instrumentation-openai/tests/traces/test_embeddings.py b/packages/opentelemetry-instrumentation-openai/tests/traces/test_embeddings.py index 931d153506..a1388355ac 100644 --- a/packages/opentelemetry-instrumentation-openai/tests/traces/test_embeddings.py +++ b/packages/opentelemetry-instrumentation-openai/tests/traces/test_embeddings.py @@ -7,7 +7,7 @@ import httpx import openai import pytest -from openai import _base_client # ST-10.4: spy target for retry-aware httpx wrap +from openai import _base_client # FortifyRoot retry-attempt: spy target for retry-aware httpx wrap from opentelemetry.sdk._logs import ReadableLogRecord from opentelemetry.semconv._incubating.attributes import ( gen_ai_attributes as GenAIAttributes, diff --git a/packages/opentelemetry-instrumentation-openai/tests/traces/test_responses.py b/packages/opentelemetry-instrumentation-openai/tests/traces/test_responses.py index 702f688aca..0839fba05b 100644 --- a/packages/opentelemetry-instrumentation-openai/tests/traces/test_responses.py +++ b/packages/opentelemetry-instrumentation-openai/tests/traces/test_responses.py @@ -536,7 +536,7 @@ def test_responses_trace_context_propagation_unit(): # Set up tracing. Use the LOCAL provider directly via # ``provider.get_tracer(...)`` and DO NOT call # ``trace.set_tracer_provider(provider)`` — OTel only allows the - # global TracerProvider to be set once per process. The ST-10.4 + # global TracerProvider to be set once per process. The FortifyRoot # retry-attempt test suite (which runs before this test under # ``--all``) already sets a global provider via its ``fresh_tracer`` # fixture; a second ``set_tracer_provider`` call would silently @@ -544,7 +544,7 @@ def test_responses_trace_context_propagation_unit(): # allowed"), leaving ``trace.get_tracer(__name__)`` bound to the # earlier global — so this test's local ``exporter`` would never # receive spans and the assertion `len(parent_spans) == 1` would - # fail in full-suite order (review-driven 2026-05-16 fix). + # fail in full-suite order. provider = TracerProvider() exporter = InMemorySpanExporter() from opentelemetry.sdk.trace.export import SimpleSpanProcessor diff --git a/packages/traceloop-sdk/tests/test_association_properties.py b/packages/traceloop-sdk/tests/test_association_properties.py index 3f5478d78c..f5431e3681 100644 --- a/packages/traceloop-sdk/tests/test_association_properties.py +++ b/packages/traceloop-sdk/tests/test_association_properties.py @@ -9,8 +9,8 @@ from traceloop.sdk import Traceloop from traceloop.sdk.decorators import task, workflow -# ST-10.4 (review-driven 2026-05-16, renamed 2026-06-12): generalized -# from name-based LangChain-only filter to role-based. Drops every +# FortifyRoot retry-attempt filtering generalized from a name-based +# LangChain-only filter to a role-based filter. Drops every # provider's FortifyRoot LLM-attempt sibling (openai / anthropic / # bedrock / langchain / llamaindex / litellm) uniformly. _FR_SPAN_ROLE_KEY = "fortifyroot.span.role" diff --git a/packages/traceloop-sdk/tests/test_privacy_no_prompts.py b/packages/traceloop-sdk/tests/test_privacy_no_prompts.py index f4d844a998..571bc0dc02 100644 --- a/packages/traceloop-sdk/tests/test_privacy_no_prompts.py +++ b/packages/traceloop-sdk/tests/test_privacy_no_prompts.py @@ -12,7 +12,7 @@ from traceloop.sdk.decorators import workflow, task -# ST-10.4: FortifyRoot LLM-attempt spans land in this test's +# FortifyRoot retry-attempt: FortifyRoot LLM-attempt spans land in this test's # session-scoped exporter ahead of the logical ``openai.chat`` span. # Filter by the canonical ``fortifyroot.span.role`` attribute so every # provider is dropped uniformly. diff --git a/packages/traceloop-sdk/tests/test_prompt_management.py b/packages/traceloop-sdk/tests/test_prompt_management.py index e408d9aa42..fca2f68f08 100644 --- a/packages/traceloop-sdk/tests/test_prompt_management.py +++ b/packages/traceloop-sdk/tests/test_prompt_management.py @@ -13,7 +13,7 @@ from traceloop.sdk.prompts.client import PromptRegistryClient -# ST-10.4: filter FortifyRoot LLM-attempt sibling spans out of legacy +# FortifyRoot retry-attempt: filter FortifyRoot LLM-attempt sibling spans out of legacy # exact-span-list / ``spans[0]`` assertions. Role-based filter so every # provider is dropped uniformly. See retry-loop docs for context. _FR_SPAN_ROLE_KEY = "fortifyroot.span.role" diff --git a/packages/traceloop-sdk/tests/test_sdk_initialization.py b/packages/traceloop-sdk/tests/test_sdk_initialization.py index 6873842747..8f31be1c3e 100644 --- a/packages/traceloop-sdk/tests/test_sdk_initialization.py +++ b/packages/traceloop-sdk/tests/test_sdk_initialization.py @@ -8,7 +8,7 @@ from traceloop.sdk.decorators import workflow -# ST-10.4: FortifyRoot LLM-attempt spans land in the exporter ahead of +# FortifyRoot retry-attempt: FortifyRoot LLM-attempt spans land in the exporter ahead of # the logical ``openai.chat`` span; filter by role so the legacy # ``spans[0] == openai.chat`` assumption survives. The user's custom # span_postprocess_callback is intentionally still invoked on the diff --git a/packages/traceloop-sdk/tests/test_workflows.py b/packages/traceloop-sdk/tests/test_workflows.py index 0b88f062bf..0fbc6283ed 100644 --- a/packages/traceloop-sdk/tests/test_workflows.py +++ b/packages/traceloop-sdk/tests/test_workflows.py @@ -17,7 +17,7 @@ from traceloop.sdk.decorators import workflow, task -# ST-10.4: filter FortifyRoot LLM-attempt sibling spans out of legacy +# FortifyRoot retry-attempt: filter FortifyRoot LLM-attempt sibling spans out of legacy # exact-span-list / set assertions. Role-based filter so every provider # (openai, anthropic, bedrock, langchain, llamaindex, litellm) is # dropped uniformly. See retry-loop docs for context.