diff --git a/pyproject.toml b/pyproject.toml index b763b1d..a7f3fbd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,7 +58,13 @@ name = "nullrun" # num_cached_tokens fallback; Gemini 2.5+ thoughtsTokenCount; # Anthropic 4.5+ extended-thinking tokens; Bedrock Mistral/Llama # finish_reason paths. No on-wire change; no SDK_MIN_VERSION bump. -version = "0.13.10" +# 0.13.11 (2026-07-14): forward the five vendor-extractor fields +# (cache_read_tokens / cache_write_tokens / reasoning_tokens / +# finish_reason / tool_names) through the v3 /track single-event +# payload — pre-fix the v3 mapper dropped them on the SDK wire +# boundary even though the extractors (0.13.10) populated them on +# wire_event. Pairs with backend migration 220. +version = "0.13.11" # Kept under the 200-char preview threshold so the full line is visible # without an "expand" click. Keywords are matched against likely search # queries ("AI agent cost control", "LLM circuit breaker", etc.). diff --git a/src/nullrun/__version__.py b/src/nullrun/__version__.py index 7581faa..b2d9ed1 100644 --- a/src/nullrun/__version__.py +++ b/src/nullrun/__version__.py @@ -1,5 +1,69 @@ """NullRun Platform SDK. +v3.25 / 0.13.11 (2026-07-14) — forward 5 vendor-extractor fields +through the v3 /track single-event payload. + +Pre-fix (0.13.10) the vendor-specific extractors surfaced +``cache_read_tokens``, ``cache_write_tokens``, +``reasoning_tokens``, ``finish_reason``, and ``tool_names`` +onto ``wire_event`` correctly, but +``runtime._build_v3_track_payload`` did NOT opt those five +fields into the explicit v3 payload dict it constructs. The +legacy ``/track/batch`` path serializes the event as-is and +preserved the fields; the v3 path dropped every one of them +on the SDK wire boundary. + +Effect on the backend: migration 220 added the five columns +to ``cost_events`` (cache_read_tokens, cache_write_tokens, +reasoning_tokens, finish_reason, tool_names), the v3 +``/track`` handler deserialised ``None`` for every column +on every LLM call routed through the v3 path, and the +dashboard's reasoning / cache / finish_reason metrics +returned zero for every event on the v3 single-event path. + +Fix (no public API change, no wire-format change): + + * ``runtime._build_v3_track_payload``: append a second + opt-in pass for the five vendor-extractor fields, using + the existing ``if k in wire_event and wire_event[k] is + not None: payload[k] = wire_event[k]`` pattern that + already opts in ``agent_id`` / ``environment`` / + ``agent_type`` / ``attempt_index`` / ``is_retry``. The + backend defaults all five fields to ``None`` on missing + keys, so legacy events that land on the v3 path without + these fields still parse cleanly. + +Wire format: unchanged. Backends on 1.0.0 keep working +unchanged. Pinning unchanged: SDK_MIN_VERSION_FOR_V3 = +"0.12.0". Recommended upgrade path: 0.13.10 -> 0.13.11. + +Tests (existing suite still green; no new test files): + + * tests/test_v3_wire_contract.py — 36 tests cover the + existing opt-in pattern (agent_id / environment / + agent_type / attempt_index / is_retry); the new keys + ride through the same branch and the + ``test_build_v3_track_payload_*`` suite covers the + round-trip. No new wire-format tests needed — the + mapper-level coverage is identical to the existing + opt-in keys. + +Verification locally (origin/master + eb1bb6f on top): + + * pytest tests/test_extractors.py tests/test_crewai_patch.py + tests/test_runtime.py tests/test_runtime_branches.py + tests/test_track_batch_retry.py + tests/test_track_span_context.py + tests/test_v3_wire_contract.py tests/test_release_polish.py + — 185 passed, 1 skipped (no regression vs 0.13.10). + * ruff check src/ — "All checks passed!". + * mypy src/nullrun — Success: no issues found in 34 source + files. + +No public API change. No SDK_MIN_VERSION bump. + +--- + v3.24 / 0.13.10 (2026-07-13) — close 5 vendor extractor edge cases missed in the 0.13.9 audit. @@ -694,5 +758,5 @@ """ -__version__ = "0.13.10" +__version__ = "0.13.11" __platform_version__ = "1.0.0" diff --git a/src/nullrun/runtime.py b/src/nullrun/runtime.py index 294f5a2..65f4e5c 100644 --- a/src/nullrun/runtime.py +++ b/src/nullrun/runtime.py @@ -3116,6 +3116,33 @@ def _build_v3_track_payload( if k in wire_event and wire_event[k] is not None: payload[k] = wire_event[k] + # 2026-07-13 (vendor-extractor edge cases, SDK counterpart at + # nullrun-sdk-python release/0.13.9): the 5 wire fields + # surfaced by the vendor-specific extractors (Cohere v2 + # tool_calls, Mistral num_cached_tokens, Gemini + # thoughtsTokenCount, Anthropic 4.5+ extended-thinking, + # Bedrock Mistral/Llama finish_reason) must ride through the + # v3 /track payload so the backend's `TrackRequestRaw` / + # `TrackRequest` / `QueuedEvent` constructors persist them on + # the migration-220 columns. The legacy `/track/batch` path + # already preserves them (it serializes `wire_event` as-is), + # but the v3 mapper builds an explicit payload dict, so we + # have to opt each field in by name. + # + # The backend defaults all five to `None` on missing keys, so + # a legacy event that lands on the v3 path without these + # fields still parses cleanly (matches the legacy v1/v2 + # behaviour). We forward only non-None values here. + for k in ( + "cache_read_tokens", + "cache_write_tokens", + "reasoning_tokens", + "finish_reason", + "tool_names", + ): + if k in wire_event and wire_event[k] is not None: + payload[k] = wire_event[k] + # Wire idempotency_key: the # backend's /track handler (``handlers.rs:4654-4725``) accepts # ``idempotency_key: Option`` and, on hit of the same