Skip to content

Commit 371545a

Browse files
Migrate OTel observer LLM span to typed event (#142)
* Migrate OTel observer LLM span to typed event Drive the openarmature.llm.complete span lifecycle from the typed LlmCompletionEvent on the success path. The span opens and closes in one shot at typed-event arrival, with start_time back-dated by latency_ms so the duration reflects the adapter-boundary measurement rather than dispatcher queue delay. Failure-path spans continue to fire from the sentinel NodeEvent pair (the typed event is success-only per proposal 0049 alternative 3). The §5.5 attribute set is unchanged. Flip OpenAIProvider.populate_caller_metadata default from False to True so the bundled OTel and Langfuse observers can emit the §5.6 openarmature.user.<key> span-attribute family without callers having to opt in. The spec-defined opt-in mechanism is preserved; only the python default flips. * Address PR 142 review Pass the typed LlmCompletionEvent to attribute_enrichers on the success path instead of None; widens the enricher signature to accept LlmCompletionEvent so enrichers can read LLM-call context that the sentinel-pair path used to expose via the closing NodeEvent. Switch the shared typed-event test helper's default node_name and namespace to "ask" so they reflect the calling-node semantics the typed event documents, not the legacy sentinel value.
1 parent cf76c53 commit 371545a

8 files changed

Lines changed: 530 additions & 311 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,15 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). The
66

77
## [Unreleased]
88

9+
### Changed
10+
11+
- **OTel observer drives the `openarmature.llm.complete` span lifecycle from the typed `LlmCompletionEvent`** (proposal 0049 + 0057, observability §5.5.7). Successful LLM-provider calls now open + close the span in one shot at typed-event arrival, with `start_time` back-dated by `LlmCompletionEvent.latency_ms` so the span duration reflects the actual adapter-boundary measurement rather than dispatcher queue delay. Failure-path spans continue to fire from the sentinel `NodeEvent` pair (the typed event is success-only per the proposal). The §5.5 attribute set is unchanged. Dual-emit window: the provider still emits both the sentinel pair AND the typed event during v0.13.0; the sentinel pair drops in v0.15.0.
12+
- **`OpenAIProvider(populate_caller_metadata=...)` default flipped from `False` to `True`.** The python implementation now populates `LlmCompletionEvent.caller_invocation_metadata` by default so the bundled OTel and Langfuse observers can emit the §5.6 `openarmature.user.<key>` span-attribute family without a separate opt-in. Pass `populate_caller_metadata=False` to suppress the snapshot when no downstream consumer needs it. The spec-defined opt-in mechanism is unchanged; only the python default flips.
13+
14+
### Added
15+
16+
- **`LlmCompletionEvent` extended with proposal 0057 request-side fields** (spec v0.51.0). The typed event now carries `input_messages`, `output_content`, `request_params`, `request_extras`, `active_prompt`, `active_prompt_group`, `call_id`, and `response_model` alongside the existing v0.49.0 fields. `request_id` renamed to `response_id` per the proposal's response-side naming. Inline image bytes in `input_messages` stay redacted per observability §5.5.5 — the OpenAI provider reuses the existing message-serialization helper for the projection. Observer-side privacy gates (OTel `disable_llm_payload`, Langfuse equivalents) apply at rendering, symmetric with the §5.5.1 span attribute path.
17+
918
## [0.12.0] — 2026-06-05
1019

1120
Observability release. The pinned spec advances from v0.38.0 to v0.46.0, absorbing eight accepted proposals (0047-0054). Three ship as fully implemented this cycle: proposal 0048 grows a read-symmetric `get_invocation_metadata()` API + a §9 *Queryable observer pattern* concept doc section; proposal 0052 puts `openarmature.implementation.name` + `.version` attribution attributes on every OTel invocation span + every Langfuse Trace; proposal 0054 ships `CompiledGraph.drain_events_for(invocation_id, *, timeout)` as the architectural pair to 0048's §9.4 accumulator lifecycle. Two ship as textual-only acks (0051 Langfuse trace I/O caveat; 0053 §3.4 shared-parent boundary clarification). One Fixed: the retry middleware now resets the invocation-metadata ContextVar between attempts per §3.4. The production-observability example grows the queryable accumulator + drain_events_for pattern end-to-end so the new APIs have a runnable demo.

src/openarmature/graph/events.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -539,9 +539,13 @@ class LlmCompletionEvent:
539539
lifetime, unique within the run. Distinct from
540540
``response_id``.
541541
- ``caller_invocation_metadata``: optional snapshot of caller-
542-
supplied invocation metadata at LLM-call time. Populated
543-
only when the provider's opt-in flag is set (per-language
544-
mechanism); default ``None``.
542+
supplied invocation metadata at LLM-call time. Spec-defined as
543+
OPTIONAL; the python OpenAIProvider populates it by default so
544+
the bundled OTel/Langfuse observers can emit the §5.6
545+
``openarmature.user.<key>`` span-attribute family without an
546+
extra opt-in. Pass ``populate_caller_metadata=False`` to suppress
547+
the snapshot. Future non-OpenAI providers MAY default to
548+
``None``.
545549
"""
546550

547551
invocation_id: str

src/openarmature/llm/providers/openai.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ def __init__(
160160
force_prompt_augmentation_fallback: bool = False,
161161
genai_system: str = "openai",
162162
readiness_probe: Literal["models", "chat_completions", "both"] = "chat_completions",
163-
populate_caller_metadata: bool = False,
163+
populate_caller_metadata: bool = True,
164164
) -> None:
165165
self.base_url = _validate_and_normalize_base_url(base_url)
166166
self.model = model
@@ -194,12 +194,13 @@ def __init__(
194194
)
195195
self._readiness_probe = readiness_probe
196196
# Proposal 0049's caller_invocation_metadata field is OPTIONAL
197-
# on the typed LlmCompletionEvent: default absent, populated
198-
# only when the consumer opts in. The per-language opt-in
199-
# mechanism is constructor-knob here so the provider can decide
200-
# at emission time without engine-level observer introspection.
201-
# Off by default to avoid bloating every event with potentially-
202-
# large metadata snapshots when nothing downstream consumes them.
197+
# on the typed LlmCompletionEvent. The python implementation
198+
# defaults the opt-in to True because the bundled OTel and
199+
# Langfuse observers read the field to populate caller-metadata
200+
# span attributes (§5.6); leaving it off by default would
201+
# silently strip those attributes after the typed-event
202+
# migration. Pass ``populate_caller_metadata=False`` to suppress
203+
# the snapshot when no downstream consumer needs it.
203204
self._populate_caller_metadata = populate_caller_metadata
204205
self._headers: dict[str, str] = {"Content-Type": "application/json"}
205206
if api_key is not None:

0 commit comments

Comments
 (0)