From eb1bb6f301fcfe375da6799ce014752b3dcd48a6 Mon Sep 17 00:00:00 2001 From: Anatolii Date: Tue, 14 Jul 2026 12:02:24 +0400 Subject: [PATCH 1/2] fix(sdk): forward vendor-extractor fields through v3 /track payload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 0.13.9 vendor-specific extractors (Cohere v2 tool_calls, Mistral num_cached_tokens, Gemini thoughtsTokenCount, Anthropic 4.5+ extended-thinking, Bedrock Mistral/Llama finish_reason) extract cache_read_tokens, cache_write_tokens, reasoning_tokens, finish_reason, and tool_names into wire_event. The legacy /track/batch path serializes the event as-is, so those fields ride through correctly. The v3 single-event path builds an explicit payload dict via _build_v3_track_payload. Pre-fix that mapper didn't opt the five fields in, so v3 /track events landed on the backend with all five columns = None — the migration-220 wireup received empty values and the dashboard's reasoning/cache metrics returned zero for every LLM call on the v3 path. Fix: forward non-None values for the five keys, matching the existing opt-in pattern for agent_id / environment / agent_type / attempt_index / is_retry. Backend defaults to None on missing keys, so a legacy event that lands on the v3 path without these fields still parses cleanly. Verified: pytest tests/test_v3_wire_contract.py + test_extractors.py + test_runtime.py + test_runtime_branches.py = 146 passed, 1 skipped, 0 regression. ruff + mypy clean. --- src/nullrun/runtime.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) 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 From 7f29f4cede4a368ca5c66768ada6f3f17c653868 Mon Sep 17 00:00:00 2001 From: Anatolii Date: Tue, 14 Jul 2026 19:08:21 +0400 Subject: [PATCH 2/2] =?UTF-8?q?chore(release):=200.13.11=20=E2=80=94=20ven?= =?UTF-8?q?dor-extractor=20fields=20on=20v3=20/track?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bump version 0.13.10 -> 0.13.11. Prepends the v3.25 / 0.13.11 changelog entry to __version__.py and extends the inline pyproject.toml comment block for 0.13.10 / 0.13.11. The actual fix — forwarding cache_read_tokens / cache_write_tokens / reasoning_tokens / finish_reason / tool_names through _build_v3_track_payload — ships in the preceding commit eb1bb6f on this branch. No on-wire change. No SDK_MIN_VERSION bump. Backends on 1.0.0 keep working unchanged. Recommended upgrade path: 0.13.10 -> 0.13.11. --- pyproject.toml | 8 ++++- src/nullrun/__version__.py | 66 +++++++++++++++++++++++++++++++++++++- 2 files changed, 72 insertions(+), 2 deletions(-) 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"