QVAC-21691 feat[api]: full-fidelity Python SDK client#3354
Merged
Conversation
Contributor
Review StatusCurrent Status: ✅ APPROVED |
This was referenced Jul 20, 2026
Contributor
License compliance — findings detected (warn-only)Critical: 0 · High: 4 · Medium: 0
How to resolve a blocking finding:
Warn-only (shadow) mode — this check does not block merges yet. Updated automatically by the canonical license compliance workflow. NOTICE presence (advisory)Missing NOTICE (advisory, does not block):
|
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
arun-mani-j
requested changes
Jul 21, 2026
arun-mani-j
left a comment
Contributor
There was a problem hiding this comment.
Looks good overall. Shared a few nits. I thought we would drop the Client style for the direction function usage like in JS. But fine with this too.
- #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)
#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.
- 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.
… (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.
lauripiisang
force-pushed
the
QVAC-21691-python-sdk
branch
from
July 21, 2026 13:58
179a4e5 to
97ad5f8
Compare
lauripiisang
force-pushed
the
QVAC-21691-python-sdk
branch
from
July 21, 2026 14:17
97ad5f8 to
68e8337
Compare
- 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.
… 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.
lauripiisang
force-pushed
the
QVAC-21691-python-sdk
branch
from
July 21, 2026 14:34
68e8337 to
b938fe8
Compare
- 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.
- 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.
arun-mani-j
approved these changes
Jul 21, 2026
opaninakuffo
approved these changes
Jul 22, 2026
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?
Ships the full-fidelity Python client for QVAC — the Python equivalent of
@qvac/sdk, so Python apps get the same capabilities and the same behaviour asthe TypeScript SDK, generated from the one contract.
Single PR by design: the SDK contract, the generated Python client, and the
worker behaviour it depends on are CI-coupled (any
packages/sdk/contractchange re-runs the Python generator and must ship the regenerated client in the
same PR), so they can't be split cleanly. Supersedes #3208 — the
bare-rpc-pythontransport PoC is included here as the production transport.🛠️ How does it solve it?
tetherto-qvac-sdk, importtetherto.qvac_sdk(PEP 420 namespace — matches@qvac/sdkscoping, reservesthe
tetherto.*root, and leaves the bareqvacname free). The name is aproposal — open to change before publish; it's applied consistently across
the code so a rename is a mechanical find-replace. Usage is flat:
from tetherto.qvac_sdk import Client, load_model, completion, embed, translate, …(+
tetherto.qvac_sdk.models).model-type maps, error-code registries, and the pinned SDK version — all from
packages/sdk/contract/**, guarded bygenerate.py --check.translatesource-language detection and the completion tool loop (
completionOrchestrate,a new duplex contract method) run worker-side; the Python client is thin glue.
This removed a real drift (Python
linguavs JS@qvac/langdetect-text).bare-rpc-python, all three call shapes, withworker-disconnect handling (pending calls fail fast instead of hanging).
Client()), or athin wheel that resolves a version-pinned worker via
python -m tetherto.qvac_sdk install-worker(recommended) or a version-matchednpm install -g @qvac/sdk@<version>. The package version tracks@qvac/sdk(0.15.0).
packages/sdk-python/examples/(one per major capability,mirroring
packages/sdk/examples), a full README with a two-step first-run(install the package, then the worker), and a Python SDK page on the docs site.
🔌 API ([api])
The ergonomic wrappers and result types are statically typed on the flat
surface. Raw generated method stubs and models are re-exported flat for runtime
use and are also available fully-typed from
tetherto.qvac_sdk.methods/.schemas. New worker methodcompletionOrchestrateis additive (nothingremoved) and is worker plumbing, not public client API (reachable at
tetherto.qvac_sdk._completion), matching the JS SDK.✅ Testing
on-pr-sdk-python-e2e.yml(label-gated,self-hosted GPU) builds the worker, installs the
bare-rpcextra, and runs thefull suite — completion (events / tools / the
completionOrchestratetoolloop), embeddings, transcription, TTS, OCR, translate (worker-side autodetect),
VLA, cancel, and worker-disconnect. The fast PR check
(
pr-checks-sdk-python) runsgenerate.py --check+ black + ruff + mypy + themocked unit tests.
(
packages/sdk/e2e/conformance/cases.json) of 8 cases (completion, translate,embed, model load/unload, cancel, tts) — 8/8 on the Python runner and 8/8 on
the JS runner against the built worker.
plugins.pyis a fixture-only template for the low-level plugin API).--filter vla) — 8/8 against real SmolVLA + pi05.bun run build,contract:check,bun run test:unit, tsc, eslint.📝 Follow-ups
bare-rpctransport dependencies aregit-only for now, and PyPI rejects direct git references in a package's required
dependencies — so publishing waits on those landing on PyPI, after which
bare-rpcfolds into the base install. The distribution name /import root(
tetherto-qvac-sdk/tetherto.qvac_sdk) is a proposal pendingconfirmation before publish.