Skip to content

chore(release): 0.22.2 — status writer + websocket extra + max_output_tokens reserve - #118

Merged
maltsev-dev merged 1 commit into
masterfrom
release/0.22.2
Oct 16, 2026
Merged

maltsev-dev merged 1 commit into
masterfrom
release/0.22.2

Conversation

@maltsev-dev

Copy link
Copy Markdown
Member

Summary

Patch release, no protocol change. Three unrelated bugs closed in one
shipping window: a status field that structurally could not report
the failure it existed to report (backend_reachable initialised to
None and never written), a packaging extra that the SDK's own
ImportError told users to install but that pyproject.toml never
declared, and a pre-flight that priced at the input rate and so
silently under-reserved every real LLM call. None is a wire change
— all three are SDK-side. The status and packaging fixes are
behaviourally invisible to a healthy configuration; the reserve
fix is visible to anyone whose max_output_tokens is currently
being ignored.

Fixed

  • DEF-STATUS-WRITER-01 — NullRunStatus.backend_reachable
    was initialised to None in __init__ and read in status() —
    and never assigned anywhere in the SDK. Two silent consequences:
    the DEGRADED headline was gated on is False so it never fired
    (pinned to None), and the MISCONFIGURED arm was gated on
    _last_backend_attempt_at is not None so it was dead for the
    same reason. Fix: one writer _note_backend_attempt(ok) called
    from the four sites that actually learn whether the backend
    answered. "Ok" means "the backend answered", NOT "the call
    succeeded": a 402/403/429 and a 401 are the backend replying,
    and only a transport-level failure sets it False. The pair is
    read under a new lock because gate calls arrive on the tool
    thread while _authenticate runs on the caller's thread.
    (b076643, +100/-8; tests/test_status_backend_reachable_has_a_writer.py,
    tests/test_status_ws_connected_reads_real_attribute.py)

  • DEF-SDK-16 (packaging: declare the websocket extra) —
    WebSocketConnection.connect raises
    websockets library not available. Install with: pip install nullrun[websocket], but nullrun had no websocket extra.
    websockets was in uv.lock only as a transitive of
    langsmith, so the WS wire test's coverage depended on a
    dependency edge nobody here controls. Now declared with floor
    14.0 (the additional_headers spelling replaced
    extra_headers in the asyncio client in 14.0; an older install
    would TypeError at connect time, later and in production
    traffic, instead of at the ImportError the user was told to
    fix). dev also gains websockets so tests/test_ws_push.py
    resolves through a declared edge. A new source pin asserts
    the general property: every nullrun[<extra>] install hint
    appearing anywhere under src/ must name an extra
    pyproject.toml actually declares. (a8cb823, +24/-1;
    tests/test_websocket_extra_is_declared.py)

Added

  • DEF-MOT-01 (reserve: forward caller-declared
    max_output_tokens to /gate)
    — per ADR-016 §2.1 the
    backend prices an output reserve from TWO independent signals
    (the caller's declared cap and its own per-model catalog cap)
    and refuses to reserve from either one alone. The SDK's half
    of that contract is now narrow and specific: when the caller
    set a cap it is sent, when the caller did not the key is
    ABSENT (not present-and-null — a null is a value the backend
    has to defend against, not an honest "I don't know"), and a
    cap of zero or less is treated as "not declared" rather than
    silently under-reserving. New ContextVar
    _call_max_output_tokens_var, reader
    get_call_max_output_tokens(), and a max_output_tokens=
    parameter on set_call_context(...). The via-edge box's
    token_split() falls back to the declared cap before the
    one-token estimate; every step down errs toward charging
    MORE, never less. (f095c46, +251/-9;
    tests/test_max_output_tokens_reserve.py)

Verification

Check Result
ruff check src tests All checks passed
mypy src/nullrun Success: no issues found in 39 source files
pytest -q 1913 passed, 12 skipped in 156.34s (vs baseline 1849 / 12 — +64 new tests from test_status_backend_reachable_has_a_writer, test_status_ws_connected_reads_real_attribute, test_websocket_extra_is_declared, test_max_output_tokens_reserve)
Scratch diff clean (dist_local/, *.defect* absent)
nullrun.__version__ 0.22.2
Wire-format compatibility unchanged from 0.22.1 (no protocol change; the max_output_tokens key is new-but-OPTIONAL, the backend ignores its absence as before)

Commits included

b076643 fix(status): backend_reachable was structurally pinned to None
a8cb823 fix(packaging): DEF-SDK-16 — declare the `websocket` extra the ImportError names
f095c46 feat(reserve): forward caller-declared max_output_tokens to /gate (DEF-MOT-01)
e56241b fix errors

@codecov

codecov Bot commented Oct 16, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.10345% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/nullrun/runtime.py 90.00% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

…_tokens reserve

## Summary

Patch release, no protocol change. Three unrelated bugs closed in one
shipping window: a status field that structurally could not report
the failure it existed to report (`backend_reachable` initialised to
`None` and never written), a packaging extra that the SDK's own
`ImportError` told users to install but that `pyproject.toml` never
declared, and a pre-flight that priced at the input rate and so
silently under-reserved every real LLM call. None is a wire change
— all three are SDK-side. The status and packaging fixes are
behaviourally invisible to a healthy configuration; the reserve
fix is visible to anyone whose `max_output_tokens` is currently
being ignored.

### Fixed

- **DEF-STATUS-WRITER-01** — `NullRunStatus.backend_reachable`
  was initialised to `None` in `__init__` and read in `status()` —
  and never assigned anywhere in the SDK. The comment above the
  initialisation claimed the pair was "set in `_authenticate` and
  updated on every successful / failed backend call thereafter";
  no such writer had ever existed. Two silent consequences: the
  DEGRADED headline was gated on `is False` so it never fired
  (pinned to `None`), and the MISCONFIGURED arm was gated on
  `_last_backend_attempt_at is not None` so it was dead for the
  same reason. Fix: one writer `_note_backend_attempt(ok)` called
  from the four sites that actually learn whether the backend
  answered. "Ok" means "the backend answered", NOT "the call
  succeeded": a 402/403/429 and a 401 are the backend replying,
  and only a transport-level failure sets it False. Collapsing
  those would brand a healthy server that correctly refused a
  call as unreachable and drive DEGRADED off a credential
  problem. The pair is read under a new lock because gate calls
  arrive on the tool thread while `_authenticate` runs on the
  caller's thread. (`b076643`, +100/-8; test files
  `test_status_backend_reachable_has_a_writer.py`,
  `test_status_ws_connected_reads_real_attribute.py`)

- **DEF-SDK-16 (packaging: declare the `websocket` extra)** —
  `WebSocketConnection.connect` raises
  `websockets library not available. Install with: pip install
  nullrun[websocket]`, but `nullrun` had no `websocket` extra.
  `[project.optional-dependencies]` declared exactly two — the
  opentelemetry pair and `dev` — and `websockets` appeared
  nowhere in `pyproject.toml`. Two declarations, not one:
  `websocket = ["websockets>=14.0,<17.0"]` and `dev` also gains
  `websockets` (so `tests/test_ws_push.py` resolves through a
  declared edge, not through `langchain-core -> langsmith ->
  websockets` which nobody here controls). A new source pin
  asserts the general property: every `nullrun[<extra>]` install
  hint appearing anywhere under `src/` must name an extra
  `pyproject.toml` actually declares. (`a8cb823`, +24/-1;
  `tests/test_websocket_extra_is_declared.py`)

### Added

- **DEF-MOT-01 (reserve: forward caller-declared
  `max_output_tokens` to `/gate`)** — per ADR-016 §2.1 the
  backend prices an output reserve from TWO independent signals
  (the caller's declared cap and its own per-model catalog cap)
  and refuses to reserve from either one alone. The SDK's half
  of that contract is now narrow and specific: when the caller
  set a cap it is sent, when the caller did not the key is
  ABSENT (not present-and-null — a null is a value the backend
  has to defend against, not an honest "I don't know"), and a
  cap of zero or less is treated as "not declared" rather than
  silently under-reserving. New ContextVar
  `_call_max_output_tokens_var`, reader
  `get_call_max_output_tokens()`, and a `max_output_tokens=`
  parameter on `set_call_context(...)`. The via-edge box's
  `token_split()` falls back to the declared cap before the
  one-token estimate; every step down errs toward charging
  MORE, never less. (`f095c46`, +251/-9; test file
  `tests/test_max_output_tokens_reserve.py`)

### Verification

| Check | Result |
|---|---|
| `ruff check src tests` | All checks passed |
| `mypy src/nullrun` | Success: no issues found in 39 source files |
| `pytest -q` | **1913 passed, 12 skipped** in 156.34s (vs baseline 1849 / 12 — **+64 new tests** from `test_status_backend_reachable_has_a_writer`, `test_status_ws_connected_reads_real_attribute`, `test_websocket_extra_is_declared`, `test_max_output_tokens_reserve`) |
| Scratch diff | clean (`dist_local/`, `*.defect*` absent) |
| `nullrun.__version__` | `0.22.2` |
| Wire-format compatibility | unchanged from 0.22.1 (no protocol change; the `max_output_tokens` key is new-but-OPTIONAL, the backend ignores its absence as before) |

### Commits included

```
b076643 fix(status): backend_reachable was structurally pinned to None
a8cb823 fix(packaging): DEF-SDK-16 — declare the `websocket` extra the ImportError names
f095c46 feat(reserve): forward caller-declared max_output_tokens to /gate (DEF-MOT-01)
e56241b fix errors
```
@maltsev-dev
maltsev-dev merged commit c72eba9 into master Oct 16, 2026
6 checks passed
@maltsev-dev
maltsev-dev deleted the release/0.22.2 branch October 16, 2026 16:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant