QVAC-21806 feat: prototype bare-rpc-python as the wire transport#3208
Closed
lauripiisang wants to merge 6 commits into
Closed
QVAC-21806 feat: prototype bare-rpc-python as the wire transport#3208lauripiisang wants to merge 6 commits into
lauripiisang wants to merge 6 commits into
Conversation
lauripiisang
force-pushed
the
QVAC-21805-python-models
branch
2 times, most recently
from
July 10, 2026 11:19
317191a to
ea37864
Compare
lauripiisang
force-pushed
the
QVAC-21806-bare-rpc-python
branch
2 times, most recently
from
July 10, 2026 15:53
9c61af3 to
099064e
Compare
lauripiisang
force-pushed
the
QVAC-21805-python-models
branch
from
July 13, 2026 21:56
fd3f50b to
feb6336
Compare
lauripiisang
force-pushed
the
QVAC-21806-bare-rpc-python
branch
from
July 13, 2026 21:58
099064e to
30691d0
Compare
lauripiisang
force-pushed
the
QVAC-21805-python-models
branch
2 times, most recently
from
July 15, 2026 15:28
2e47338 to
765abea
Compare
- BareRpcWorker wraps bare_rpc.RPC directly against the spawned worker's Unix socket, replacing poc_heartbeat.QvacWorker's hand-rolled framing for the unary and server-stream call shapes - its call/call_stream already satisfy qvac._transport.Transport's async shape, so it plugs straight into the real generated qvac.methods stubs with no PocTransport-style adapter - bare-rpc-python/compact-encoding-python aren't on PyPI yet -- pinned as an optional "bare-rpc" extra via git dependencies
Same fix as the QVAC-21805-python-models commit, applied to the 21806-only bare_rpc PoC files: poc_bare_rpc_transport.py's SDK default was a hardcoded developer-specific absolute path; default to ../../sdk relative to this file instead, and gate test skips on the worker actually existing rather than on QVAC_POC_SDK_DIR being set.
…am runtime - BareRpcWorker.call_duplex maps onto RPC.create_bidirectional_stream: write the JSON payload as the first outgoing chunk followed by up's chunks, end() the outgoing side, and read incoming with the same buffer-and-split-on-newline handling as call_stream. The upstream pump runs as a concurrent background task since the worker can start responding before the client finishes sending. - BareRpcWorker now implements all three Transport call shapes (call/call_stream/ call_duplex), so it plugs directly into the generated duplex stubs (transcribe_stream, text_to_speech_stream, bci_transcribe_stream) with no PocTransport-style adapter. - New test: load a real TTS model and drive text_to_speech_stream end to end against a spawned SDK worker, asserting real synthesized audio and a terminal done=True event.
…f erroring - test_poc_bare_rpc.py's skipif only covered the missing-built-worker case; the unconditional `import bare_rpc` at module level still hard-failed collection in CI, where the pr-checks-sdk-python.yml install step (`.[gen,dev]`) doesn't pull in the unpublished `bare-rpc` extra. - Wrap the import, expose BARE_RPC_AVAILABLE, and add it as a second skip condition alongside the existing worker-not-built check.
…erage to transcribe_stream and bci_transcribe_stream - New src/qvac/bare_rpc_transport.py: BareRpcTransport implements qvac._transport.Transport over bare_rpc.RPC, mirroring the JS SDK's client/rpc/node-rpc-client.ts (socket handshake + worker spawn), with the caller supplying the Bare command instead of this package resolving/bundling worker artifacts itself. - Retire tests/poc_bare_rpc_transport.py; test_poc_bare_rpc.py becomes test_bare_rpc_transport.py, testing the production module directly. - Add duplex tests for the two remaining duplex-shaped methods beyond text_to_speech_stream: transcribe_stream (parakeet, real-time-paced 16kHz mono s16le PCM) and bci_transcribe_stream (BCI addon, raw neural feature chunks against a real fixture). - Full sdk-python suite (39 tests) passes against a real spawned worker.
…gates Rebasing onto merged #3100 pulls in ruff+mypy CI for sdk-python; the bare-rpc transport predates it. Bring it into line: - ruff: typing.AsyncIterable/AsyncIterator/Sequence -> collections.abc, drop redundant forward-ref quotes, sort the test import block - mypy: assert post-connect invariants (rpc/writer set) and add a _require_rpc guard so the Transport methods don't deref Optional; add a scoped ignore_missing_imports override for the optional, git-only bare_rpc/compact_encoding extras - test: load the Qwen3 completion model with an explicit n_ctx -- it's a thinking model, so the worker reserves context for the reasoning trace and the metadata-default budget overflowed even a tiny prompt
lauripiisang
force-pushed
the
QVAC-21806-bare-rpc-python
branch
from
July 16, 2026 22:37
9d55aa7 to
c9d5d60
Compare
| if not chunk: | ||
| break | ||
| await self.rpc.receive(chunk) | ||
| except asyncio.CancelledError: |
| self._server.close() | ||
| try: | ||
| await asyncio.wait_for(self._server.wait_closed(), timeout=5) | ||
| except asyncio.TimeoutError: |
| pump_task.cancel() | ||
| try: | ||
| await pump_task | ||
| except asyncio.CancelledError: |
This was referenced Jul 20, 2026
Contributor
Author
|
Superseded by #3354 — the bare-rpc-python transport prototyped here ships as the production transport inside the full-fidelity Python SDK PR. |
lauripiisang
added a commit
that referenced
this pull request
Jul 20, 2026
…sdk) The Python equivalent of @qvac/sdk: same capabilities, same behaviour, one contract. Distribution `tetherto-qvac-sdk`, import `tetherto.qvac_sdk` (PEP 420 namespace — proposed name, applied consistently). Supersedes the #3208 transport PoC as the production transport. - flat API: `from tetherto.qvac_sdk import Client, load_model, completion, …` (+ `.models`); ergonomic wrappers over generated stubs, re-exported flat - generated from packages/sdk/contract: pydantic models, typed async method stubs, model-type maps, error-code registries, pinned SDK version; guarded by generate.py --check - typed error hierarchy + cross-RPC reconstruction; loadModel type inference; duplex streaming sessions; VLA numpy/base64 marshaling (offloaded off the event loop); client logging; notebook SyncClient (numpy/pandas) - bare-rpc transport, all three call shapes, failing pending calls on worker disconnect instead of hanging - worker resolution (bundled wheel / QVAC_SDK_DIR / install-worker / global npm), version locked to @qvac/sdk; self-contained wheel + worker-bootstrap scripts - completion_orchestrate kept off the flat public API (JS has no client wrapper) - unit + real-worker e2e tests, runnable examples (one per capability + a notebook facade demo), and a full README ship with the package
lauripiisang
added a commit
that referenced
this pull request
Jul 21, 2026
…sdk) The Python equivalent of @qvac/sdk: same capabilities, same behaviour, one contract. Distribution `tetherto-qvac-sdk`, import `tetherto.qvac_sdk` (PEP 420 namespace -- proposed name, applied consistently). Supersedes the #3208 transport PoC as the production transport. - flat API: `from tetherto.qvac_sdk import Client, load_model, completion, …` (+ `.models`); ergonomic wrappers over generated stubs, re-exported flat - generated from packages/sdk/contract: pydantic models, typed async method stubs, model-type maps, error-code registries, pinned SDK version; guarded by generate.py --check - typed error hierarchy + cross-RPC reconstruction; loadModel type inference; duplex streaming sessions; VLA numpy/base64 marshaling (off the event loop); client logging; notebook SyncClient (numpy/pandas) - bare-rpc transport, all three call shapes, failing pending calls on worker disconnect instead of hanging; per-platform Bare resolution + exec-bit - Client/BareRpcTransport `config=` sends QvacConfig (cacheDirectory, loggerLevel, …) on connect, mirroring the JS client (QVAC_CACHE_DIR / QVAC_HOME_DIR env conveniences) - worker resolution (bundled wheel / QVAC_SDK_DIR / install-worker / global npm), version-locked; self-contained wheel + worker-bootstrap scripts - completion_orchestrate kept off the flat public API (JS has no client wrapper) - unit + real-worker tests (CLI minimal-model set; heavy parakeet/BCI/VLA marked `heavy`), runnable examples per capability (+ a notebook facade demo), and a full README ship with the package
opaninakuffo
added a commit
that referenced
this pull request
Jul 22, 2026
* QVAC-21691 feat: worker + contract support for the Python client Worker-side and contract changes the Python client depends on, kept in one PR because the contract, the generated client, and the worker behaviour are CI-coupled. - move translate source-language detection into the worker (was per-client: Python lingua vs JS langdetect-text drifted); wire `from` stays an ISO code, expanded to the language name by the existing getLanguage() - add completionOrchestrate: a worker-run tool loop over a new duplex contract method, with a bounded, torn-down tool-result reader - shared cross-client conformance corpus (e2e/conformance/cases.json) + JS runner, on the minimal CLI model set (Qwen 600M, EmbeddingGemma, Whisper EN-tiny, Supertonic) - bound the logging-streaming example's generation so it can't overflow ctx * QVAC-21691 feat[api]: full-fidelity Python SDK client (tetherto.qvac_sdk) The Python equivalent of @qvac/sdk: same capabilities, same behaviour, one contract. Distribution `tetherto-qvac-sdk`, import `tetherto.qvac_sdk` (PEP 420 namespace -- proposed name, applied consistently). Supersedes the #3208 transport PoC as the production transport. - flat API: `from tetherto.qvac_sdk import Client, load_model, completion, …` (+ `.models`); ergonomic wrappers over generated stubs, re-exported flat - generated from packages/sdk/contract: pydantic models, typed async method stubs, model-type maps, error-code registries, pinned SDK version; guarded by generate.py --check - typed error hierarchy + cross-RPC reconstruction; loadModel type inference; duplex streaming sessions; VLA numpy/base64 marshaling (off the event loop); client logging; notebook SyncClient (numpy/pandas) - bare-rpc transport, all three call shapes, failing pending calls on worker disconnect instead of hanging; per-platform Bare resolution + exec-bit - Client/BareRpcTransport `config=` sends QvacConfig (cacheDirectory, loggerLevel, …) on connect, mirroring the JS client (QVAC_CACHE_DIR / QVAC_HOME_DIR env conveniences) - worker resolution (bundled wheel / QVAC_SDK_DIR / install-worker / global npm), version-locked; self-contained wheel + worker-bootstrap scripts - completion_orchestrate kept off the flat public API (JS has no client wrapper) - unit + real-worker tests (CLI minimal-model set; heavy parakeet/BCI/VLA marked `heavy`), runnable examples per capability (+ a notebook facade demo), and a full README ship with the package * QVAC-21691 doc: Python SDK page on the docs website - add content/docs/python-sdk.mdx: install (package + required worker), the flat API, configuring the SDK (config=/QVAC_CACHE_DIR), the notebook facade, and consistency-with-the-TS-SDK notes - wire it into the sidebar (custom-tree) and link it from installation * QVAC-21691 infra: Python SDK CI — fast checks, gated fast e2e, cross-platform full e2e - pr-checks-sdk-python.yml: fast, every PR — generate --check, black, ruff, mypy, mocked unit tests (real-worker tests skip themselves here). - on-pr-sdk-python-e2e.yml: `verified`-label-gated, hosted — builds the worker and runs the fast real-worker set (`pytest -m "not heavy"`) on the minimal CLI model set. ~2 min cold. - on-pr-sdk-python-e2e-full.yml: `test-e2e-full`-gated, self-hosted win/mac/linux matrix — the full suite incl. the heavy parakeet/BCI tests, reusing the SDK e2e's model cache (via the client's cacheDirectory config; key falls back to the SDK's qvac-models-<hash>), proving the client cross-platform. * QVAC-21691 fix: review round 1 (arun #3/#4/#6/#7) - #6 __main__: print "python -m tetherto.qvac_sdk install-worker" (module was renamed; the old command failed) - #7 EmbedFailedError: move to errors.py, read code from the generated registry instead of hardcoding 52401 (only hardcoded code in the package) - #4 JS parity: add TRANSLATION_FAILED to the JS reconstructors so both clients rebuild the typed error (translate detection is now server-side) - #3 orchestrate: emit stopReason=maxToolTurns on the truncation frame so a capped run is distinguishable from a natural finish (contract + Python regen) * QVAC-21691 fix: cross-platform Python transport over loopback TCP (arun #1) - The Python transport used asyncio.start_unix_server, which is Unix-only, so every real-worker path was broken on Windows while Windows was in the e2e matrix. asyncio has no cross-platform server for Unix sockets or named pipes. - Bind a loopback TCP port (127.0.0.1:0) on every OS and hand the worker a tcp://127.0.0.1:<port> endpoint. One code path everywhere; the Linux/macOS e2e legs exercise the exact path Windows uses. - Worker: createIPCClient now dials tcp:// endpoints via connect(port, host), falling back to the filesystem-socket/named-pipe path the Node client uses. - Tests: _worker_env resolves bare.exe as well as bare, so the Windows leg runs the real-worker suite instead of silently skipping it. Validated: connect, unary, streaming completion, config passthrough, and worker-disconnect fast-fail all pass over the TCP transport. * QVAC-21691 fix: make cancel reach a running orchestrate turn (arun #2) - orchestrateCompletion dropped the orchestrate requestId, so each inner completionStream turn ran under a server-generated id the client never saw; cancel({requestId}) matched nothing and the Stop button was a no-op. - Thread the orchestrate requestId into every turn. Turns run sequentially, so turn N's registry entry is disposed before turn N+1 begins -- no id collision -- and a cancel that lands between turns is caught by the registry's cancel-before-begin tripwire. - Add a real-worker e2e (test_completion_orchestrate_cancel_stops_generation): start a long generation, wait for the first delta, cancel by requestId, assert the run raises InferenceCancelledError. Plus JS unit guards: both turns carry the orchestrate id, the cap frame is marked stopReason=maxToolTurns, and a natural finish leaves stopReason unset. * QVAC-21691 test: cover the worker tool loop in the conformance corpus (arun #5) - completionOrchestrate was absent from the shared corpus, so the worker-driven tool loop -- the feature most likely to drift -- had no cross-client check. - Add an orchestrate-tool-loop case (data-driven tool spec + fixed result so it stays language-neutral). The Python runner drives it via completion_orchestrate and asserts the final answer reflects the tool result. - The JS client has no worker-loop wrapper (it orchestrates tool loops client-side), so its runner logs a documented SKIP instead of reimplementing the loop. The corpus description records the asymmetry. Validated cold on both runners: Python case passes; JS runner 9/9 with the orchestrate case skipped. * QVAC-21691 infra: run full e2e on the runners' pre-provisioned toolchain - Per infra, jobs must not run toolchain "setup" on the self-hosted GPU runners; Python, Node, and bun are already provisioned there. - Drop actions/setup-python (it also hard-fails: its python-versions tarball hardcodes `mkdir /Users/runner/hostedtoolcache`, unwritable because the runner user's home is /Users/actions-runner-*), plus actions/setup-node and oven-sh/setup-bun. Use the present Python via a venv (guarded at >=3.10) and the present node/bun directly. - Save the model cache even on test failure so the multi-GB models fetched this run aren't re-downloaded next time. * QVAC-21691 fix: Windows portability (utf-8 generation + path-agnostic tests) - generate.py wrote/read files without an explicit encoding, so on Windows (cp1252 default) generated modules were emitted as non-UTF-8 -- an em-dash in a docstring became byte 0x97, and every UTF-8 reader (black, the import machinery) then failed. Pin encoding="utf-8" on all read_text/write_text. - client.py read the SDK package.json without encoding; pin it too. - test_client.py asserted derived worker/bare paths against POSIX string literals; the code returns OS-native paths, so compare via pathlib (Path(...) / .as_posix()) to pass on Windows without weakening the check. Validated on the full cross-platform e2e leg: the transport and all runtime functionality already pass on Windows; these fix the generation + path-string test failures that were Windows-only. * QVAC-21691 infra: run full e2e using the runners' provisioned toolchain - Per infra, don't run actions/setup-python on the self-hosted GPU runners: Python is already provisioned, and setup-python hard-fails anyway (its python-versions tarball hardcodes `mkdir /Users/runner/hostedtoolcache`, unwritable because the runner user's home is /Users/actions-runner-*). Use the present Python via a venv (guarded at >=3.10). - node/bun are still set up with their actions: unlike setup-python they work on these runners (they find the pre-cached tool, no download), and bun is not otherwise on PATH. These are the same actions the SDK's own desktop e2e uses. - Save the model cache even on test failure so the multi-GB models fetched this run aren't re-downloaded next time. * QVAC-21691 fix: Windows generation determinism + parakeet stream retry - generate --check compared fresh vs committed output by filecmp stat, so on Windows the platform-default CRLF a fresh build writes flagged every file as differing from the committed LF tree. Compare newline-normalized content instead; real content drift still fails. - The parakeet streaming test asserted a transcript from a single session, but a session can yield zero events on the first attempt (a known warm-up quirk the SDK's own runner recovers from via recoveryMaxAttempts). Retry a few times before failing; it surfaced as an intermittent empty transcript on CI. - run prettier over the conformance runner so the SDK pod format check passes. --------- Co-authored-by: Opanin Akuffo <46673050+opaninakuffo@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🎯 What problem does this PR solve?
poc_heartbeat.py) hand-rolls the bare-rpc wire protocol — varint encode/decode, stream flags, message framing — because no production-grade transport existed yet (QVAC-21805).holepunchto/bare-rpc-pythonappeared as a real candidate mid-development. This PR proves it out across all three RPC call shapes (unary, server-stream, duplex) and promotes it to the production transport.📝 How does it solve it?
src/qvac/bare_rpc_transport.py:BareRpcTransportwiresbare_rpc.RPCdirectly to a spawned worker's Unix socket — no hand-rolled framing at all, justrpc.request(...)/rpc.request_with_response_stream(...)/rpc.create_bidirectional_stream(...). It mirrors the JS SDK'sclient/rpc/node-rpc-client.ts: create the socket, spawn the worker with the socket path +HOME_DIRas its JSON config argument, wirebare_rpc.RPCto the connection the worker dials back in on. Locating the Bare binary and worker entry point stays the caller's job — this is a thin client, not a self-contained package (that's separate packaging work, tracked outside this PR's scope).BareRpcTransport.call/.call_stream/.call_duplexsatisfyqvac._transport.Transport's async shape exactly, sotest_bare_rpc_transport.pygoes straight through the real generatedqvac.methodsstubs — no adapter layer needed.RPC.create_bidirectional_stream's(outgoing, incoming)pair: write the JSON payload as the first outgoing chunk, thenup's chunks,end()the outgoing side, and readincomingwith the same buffer-and-split-on-newline handling ascall_stream. The upstream pump runs as a concurrent background task, since the worker can start responding before the client finishes sending.text_to_speech_stream,transcribe_stream(parakeet, fed real-time-paced 16kHz mono PCM — its streaming session only decodes at roughly real-time cadence), andbci_transcribe_stream(the BCI addon's sliding-window driver, fed raw neural feature chunks against a real fixture, no pacing needed).bare-rpc-python/compact-encoding-pythonaren't on PyPI yet (bothv0.0.1, git-only) — pinned as an optionalbare-rpcpyproject extra via git dependencies, so they never affect the default install. Verified directly against upstream:bare-rpc-python's pin is upstream's exact current HEAD;compact-encoding-python's pin is a few commits behind upstream, butbare-rpc-pythonitself stays on that same older commit, so no bump was needed.🧪 How was it tested?
sdk-pythonsuite: 39/39 pass with these included.Status: production transport.
poc_heartbeat.py's hand-rolled codec remains for now as a reference/fallback whilebare-rpc-pythonis still unreleased, butBareRpcTransportis the transport new code should use.