Skip to content

QVAC-21691 feat[api]: full-fidelity Python SDK client#3354

Merged
opaninakuffo merged 13 commits into
mainfrom
QVAC-21691-python-sdk
Jul 22, 2026
Merged

QVAC-21691 feat[api]: full-fidelity Python SDK client#3354
opaninakuffo merged 13 commits into
mainfrom
QVAC-21691-python-sdk

Conversation

@lauripiisang

@lauripiisang lauripiisang commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

🎯 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 as
the 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/contract
change 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-python transport PoC is included here as the production transport.

🛠️ How does it solve it?

  • Scoped, flat Python API: distribution tetherto-qvac-sdk, import
    tetherto.qvac_sdk (PEP 420 namespace — matches @qvac/sdk scoping, reserves
    the tetherto.* root, and leaves the bare qvac name free). The name is a
    proposal
    — 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).
  • Generated from the contract: pydantic models + typed async method stubs,
    model-type maps, error-code registries, and the pinned SDK version — all from
    packages/sdk/contract/**, guarded by generate.py --check.
  • Behaviour lives in the worker (shared, not re-implemented): translate
    source-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 lingua vs JS @qvac/langdetect-text).
  • Production transport: bare-rpc-python, all three call shapes, with
    worker-disconnect handling (pending calls fail fast instead of hanging).
  • Distribution: a self-contained bundled wheel (zero-config Client()), or a
    thin wheel that resolves a version-pinned worker via
    python -m tetherto.qvac_sdk install-worker (recommended) or a version-matched
    npm install -g @qvac/sdk@<version>. The package version tracks @qvac/sdk
    (0.15.0)
    .
  • Docs & examples: 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])

from tetherto.qvac_sdk import Client, load_model, completion
from tetherto.qvac_sdk.models import LLAMA_3_2_1B_INST_Q4_0

async with Client() as client:
    t = client.transport
    model_id = await load_model(t, model_src=LLAMA_3_2_1B_INST_Q4_0)
    run = completion(t, model_id=model_id, history=[{"role": "user", "content": "hi"}])
    async for event in run.events:
        ...
    final = await run.final

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 method completionOrchestrate is additive (nothing
removed) and is worker plumbing, not public client API (reachable at
tetherto.qvac_sdk._completion), matching the JS SDK.

✅ Testing

  • Real-worker e2e runs in CI: on-pr-sdk-python-e2e.yml (label-gated,
    self-hosted GPU) builds the worker, installs the bare-rpc extra, and runs the
    full suite — completion (events / tools / the completionOrchestrate tool
    loop), embeddings, transcription, TTS, OCR, translate (worker-side autodetect),
    VLA, cancel, and worker-disconnect. The fast PR check
    (pr-checks-sdk-python) runs generate.py --check + black + ruff + mypy + the
    mocked unit tests.
  • Cross-client conformance: a shared corpus
    (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.
  • Examples verified end-to-end against a real worker + real models (14 of 15;
    plugins.py is a fixture-only template for the low-level plugin API).
  • VLA e2e (--filter vla) — 8/8 against real SmolVLA + pi05.
  • SDK side: bun run build, contract:check, bun run test:unit, tsc, eslint.

📝 Follow-ups

  • PyPI publish is not wired yet: the bare-rpc transport dependencies are
    git-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-rpc folds into the base install. The distribution name /import root
    (tetherto-qvac-sdk / tetherto.qvac_sdk) is a proposal pending
    confirmation before publish.

@lauripiisang
lauripiisang requested review from a team as code owners July 20, 2026 16:56
@github-actions

github-actions Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Review Status

Current Status: ✅ APPROVED
Approvals so far: Team Lead: 1, Member: 1

@github-actions

github-actions Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

License compliance — findings detected (warn-only)

Critical: 0 · High: 4 · Medium: 0

Dependency License Scope Severity Outcome
actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd (none detected) runtime High blocks
actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f (none detected) runtime High blocks
actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 (none detected) runtime High blocks
oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 (none detected) runtime High blocks

How to resolve a blocking finding:

  • Remove or replace the disallowed dependency, or
  • If the license is genuinely acceptable, run the compliance SKILL and record the decision in .github/license-allowlist.yml (CODEOWNERS-reviewed), or
  • For a one-off, a maintainer can apply the license-override label (High findings only; Critical cannot be overridden).

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):

  • ./packages/embed-llamacpp/benchmarks/server
  • ./packages/embed-llamacpp/benchmarks/performance
  • ./packages/ggml-coload-smoke
  • ./packages/llm-llamacpp/benchmarks/server
  • ./packages/llm-llamacpp/benchmarks/performance
  • ./packages/fabric/test/integration
  • ./packages/transcription-whispercpp/benchmarks/server
  • ./packages/transcription-parakeet/benchmarks/server
  • ./packages/sdk/e2e
  • ./packages/vla-ggml/sim/server
  • ./.github/actions/release-merge-guard
  • ./docs/website

Comment thread packages/sdk-python/src/tetherto/qvac_sdk/bare_rpc_transport.py Fixed
Comment thread packages/sdk-python/src/tetherto/qvac_sdk/bare_rpc_transport.py Fixed
Comment thread packages/sdk-python/src/tetherto/qvac_sdk/bare_rpc_transport.py Fixed
Comment thread packages/sdk-python/tests/test_poc_methods.py Fixed
Comment thread packages/sdk-python/tests/test_conformance.py Fixed
Comment thread packages/sdk-python/tests/test_errors.py Fixed
Comment thread packages/sdk-python/tests/test_public_api.py Fixed
Comment thread packages/sdk-python/src/tetherto/qvac_sdk/_api.py Fixed
Comment thread packages/sdk-python/tests/test_notebook.py Fixed
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@arun-mani-j arun-mani-j left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread packages/sdk-python/src/tetherto/qvac_sdk/bare_rpc_transport.py Outdated
Comment thread packages/sdk/server/rpc/handlers/completion-orchestrate.ts Outdated
Comment thread packages/sdk/server/rpc/handlers/completion-orchestrate.ts Outdated
Comment thread packages/sdk-python/src/tetherto/qvac_sdk/errors.py
Comment thread packages/sdk/e2e/conformance/cases.json
Comment thread packages/sdk-python/src/tetherto/qvac_sdk/__main__.py Outdated
Comment thread packages/sdk-python/src/tetherto/qvac_sdk/notebook.py Outdated
- #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.
- 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.
- 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

test-e2e-full Triggers full e2e test suite [Currently SDK-only] verified Authorize secrets / label-gate in PR workflows

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants