QVAC SDK v0.19.0
📦 NPM: https://www.npmjs.com/package/@qvac/sdk/v/0.19.0
QVAC SDK 0.19.0 is the first release after @qvac/inference became the in-process engine. You can assess whether a model will fit before downloading it, walk an ABot-World session, generate MiniMax music, and transcribe with Parakeet Unified. Delegated DHT inference is gone; no_mmap is load_mode, and batch translations return an array instead of a newline-joined string. @qvac/bare-sdk is no longer part of the lockstep pipeline.
Table of Contents
Breaking Changes
Inference Is the In-Process Engine
The SDK worker and Bare in-process path now sit on @qvac/inference. @qvac/bare-sdk is a thin re-export and is no longer versioned with the SDK. Import lifecycle helpers from @qvac/sdk/worker-lifecycle. The worker entry is dist/src/worker/index.js. ./commands and ./worker-core are gone; importing them throws with migration guidance. ./onnx-tts/plugin remains as an alias of the TTS plugin.
Before:
import { … } from "@qvac/sdk/worker-core";
const worker = "<sdk_root>/dist/server/worker.js";After:
import { … } from "@qvac/sdk/worker-lifecycle";
const worker = "<sdk_root>/dist/src/worker/index.js";@qvac/sdk now depends on @qvac/inference@^0.19.0. Install both at 0.19.0.
Delegated Inference Removed
Provider mode and DHT delegation are gone. Models load and run locally only.
Before:
await startQVACProvider({ firewall })
const id = await loadModel({ modelSrc, delegate: { providerPublicKey } })
await heartbeat({ delegate: { providerPublicKey } })
await stopQVACProvider()After:
const id = await loadModel({ modelSrc })
await heartbeat()Removed: startQVACProvider, stopQVACProvider, loadModel/heartbeat delegate options, hasActiveProviders on unload, isDelegated/providerInfo on loaded-model info, profiler origin / resourceOrigin, and the provider/delegate error classes. Python load_model(delegate=...) is also gone.
no_mmap Became load_mode
modelConfig.no_mmap is replaced by load_mode. Do not keep a boolean under the new key.
Before:
await loadModel({
modelSrc: MODEL,
modelType: 'llm',
modelConfig: { ctx_size: 2048, no_mmap: true }
})After:
await loadModel({
modelSrc: MODEL,
modelType: 'llm',
modelConfig: { ctx_size: 2048, load_mode: 'none' }
})| Before | After |
|---|---|
no_mmap: true | load_mode: "none" |
no_mmap: false | omit load_mode, or load_mode: "mmap" |
| omitted | omitted (addon default mmap) |
The same mapping applies to deviceDefaults.llm and deviceDefaults["llamacpp-completion"]. load_mode also accepts "mlock", "mmap+mlock", and "dio".
Batch Translations Return an Array
A translate call with several strings used to join results with \n. It now returns translations: string[]. Streaming emits one whole translation per token, in input order.
Before:
const result = translate({
modelId,
text: ['Good morning', 'Good night'],
stream: false
})
const translations = (await result.text).split('\n')After:
const result = translate({
modelId,
text: ['Good morning', 'Good night'],
stream: false
})
const translations = await result.translationsn_discarded Dropped
modelConfig.n_discarded is no longer accepted. Context overflow now reports requiredTokens, cachedTokens, promptTokens, and ctxSize on ContextOverflowError.
MiniMax Python Config Types
AudioGen load-config generated names are engine-specific. ACE-Step classes gained an Acestep infix; MiniMax has its own Minimax types. Update Python imports accordingly.
Language Detection Package Rename
@qvac/langdetect-text-cld2 is removed. Import from @qvac/langdetect-text. detectOne is synchronous.
e2e Test Suite Package Rename
SDK e2e depends on @qvac/test-suite instead of @qvac/qvac-test-suite. Update imports and Metro resolvers. The old package remains installable for 0.10.x pins.
New APIs
assessModelFit
assessModelFit estimates whether a set of models will fit before you download them. The same function is exported from @qvac/sdk and @qvac/inference.
import { assessModelFit, QWEN3_8B_INST_Q4_K_M } from '@qvac/sdk'
const result = await assessModelFit({
models: [{ model: QWEN3_8B_INST_Q4_K_M, workload: { kind: 'llm', contextTokens: 8192 } }],
execution: 'sequential',
policy: 'interactive-v1'
})
result.verdict // "likely-fits" | "likely-too-large" | "unknown"
result.basis // "system-memory" | "process-memory" | "device-memory" | "device-budget"
result.budget?.availableBytes // headroom before the policy reserveOn a discrete GPU, basis is device-memory (Linux VRAM) or device-budget (Windows DXGI). Integrated GPUs stay on system-memory because they allocate from RAM. Multi-GPU machines require likely-fits on the smallest usable card and likely-too-large on the largest; in between the verdict is unknown. VM display adapters are not counted as GPUs. The reserve is 20% of budget.availableBytes, capped at 2 GiB on desktop and 1 GiB on mobile. iOS uses per-process memory and may return unknown when that metric is missing. Catalog resource profiles (getModelResourceProfile) back the estimator; an unknown checksum is undefined, not a guess.
ABot-World Sessions
Load a world-mode diffusion model, create a scene once, then step it. Frames stream as they decode.
const modelId = await loadModel({
modelSrc: ABOT_WORLD_0_5B_Q8_0,
modelType: 'sdcpp-generation',
modelConfig: {
mode: 'world',
taehvModelSrc: ABOT_WORLD_0_5B_LF_VAE,
t5XxlModelSrc: UMT5_XXL_ENC_Q8_0,
vaeModelSrc: ABOT_WORLD_0_5B_LF_VAE_F16,
world: { kvCache: true, frameJpegQuality: 85 }
}
})
const { stats } = worldCreateScene({ modelId, prompt, image })
await stats
const { frameStream } = worldStep({ modelId, keys: ['W', 'L'] })
for await (const frame of frameStream) {
render(frame)
}Pass returnPack: true on create to keep the scene bytes for a later reload.
MiniMax Music Generation
AudioGen can load MiniMax (engine: "minimax") alongside ACE-Step.
const modelId = await loadModel({
modelType: 'audiogen',
modelConfig: {
engine: 'minimax',
lmModelSrc: '/models/mm3-lm-q8.gguf',
synthModelSrc: '/models/mm3-synth-q8.gguf'
}
})
const run = audioGen({
modelId,
caption: 'warm cinematic piano',
maxFrames: 250,
inferenceSteps: 12,
cfgScale: 1.8
})audioGen results now include diagnostics (selectedBackend, selectedDevice, optional fallback.reason when a GPU request landed on CPU). Progress total may be 0 for indeterminate stages.
Parakeet Unified Transcription
import { loadModel, transcribe, PARAKEET_UNIFIED_0_6B_Q8_0 } from '@qvac/sdk'
const modelId = await loadModel({
modelSrc: PARAKEET_UNIFIED_0_6B_Q8_0,
modelType: 'parakeet-transcription'
})
const text = await transcribe({ modelId, audioChunk: 'audio.wav' })Hugging Face Download Checksums
Hugging Face HTTP downloads are verified against the Hub SHA-256. requireHttpChecksum and requireSecureTransport can also be set globally or per loadModel / downloadAsset call. Plain HTTP to a private origin is unchanged unless you opt in.
await loadModel({
modelSrc: 'https://huggingface.co/org/repo/resolve/main/model.gguf',
modelType: 'llamacpp-completion',
requireHttpChecksum: true,
requireSecureTransport: true
})Worker Startup Timeout
rpcInitTimeoutMs (config file) and QVAC_RPC_INIT_TIMEOUT_MS control the worker handshake. A timeout whose worker already exited attaches WorkerStartupError as cause, with exitCode, exitSignal, and stderrTail, so you can tell a dead worker from a slow one.
Tensor Split and Flash Attention
llama.cpp loads accept split-mode: "tensor" and flash-attn: "on" in modelConfig.
Injected TurboVec RAG Index
Set ragTurbovec: true in qvac.config.json. Embedding plugins can supply a turbovecIndexProvider with create / load.
Config Schema Descriptions
Every modelConfig field now carries a description, exported from @qvac/sdk/schemas. configSchemaForModelType("whisper") (or "llm", "tts-ggml", "diffusion", …) returns that type's schema so tools such as qvac configure can document options without a per-addon list.
Features
Qwen3.8 tool calls go through the Qwen parser. Darwin-arm64 calibration uses a persistent-based fit with an audio guard. Desktop calibration for assessModelFit covers darwin-x64, linux-arm64, and win32-x64, including integrated GPUs; AMD linux stays unknown. @qvac/tts-ggml 0.8.0 can select CUDA on linux-x64 NVIDIA without a new backend key. @qvac/diffusion-cpp is ^0.21.0. @qvac/bci-whispercpp is 0.8.0.
Bug Fixes
A worker RPC init timeout now preserves the exit signal instead of dropping it. Expo prebuild refreshes iOS addon links. ContextOverflowError reports how many tokens the request needed versus the effective ctx_size / parallel ceiling.
Model Changes
This release adds Parakeet Unified 0.6B transcription constants and Qwen3.8 Flash Next 177B multimodal shards.
Added Models
MMPROJ_QWEN3_8_FLASH_NEXT_177B_MULTIMODAL_F16
PARAKEET_UNIFIED_0_6B_F16
PARAKEET_UNIFIED_0_6B_Q4_0
PARAKEET_UNIFIED_0_6B_Q8_0
QWEN3_8_FLASH_NEXT_177B_MULTIMODAL_UD_Q2_K_XL_SHARD
QWEN3_8_FLASH_NEXT_177B_MULTIMODAL_UD_Q4_K_XL_SHARDThe Python client for QVAC: local-first, P2P AI inference (LLM completion,
embeddings, transcription, TTS, OCR, translation, diffusion, audio generation,
VLA, …) through the same worker and the same contract as the TypeScript
@qvac/sdk. Asyncio-native.
The package version tracks the @qvac/sdk version it speaks (e.g. 0.15.0), so
a given python-sdk release makes clear exactly which SDK it targets.
Install
The fastest path is a self-contained install — one command, no separate
worker, no Node.js. It pulls a per-platform wheel that bundles the QVAC worker
(the @qvac/sdk runtime) and the Bare runtime, from the matching GitHub
release (these wheels are far too large for PyPI, so they live as release
assets):
# Replace <version> with the release you want, e.g. sdk-v0.17.0:
pip install tetherto-qvac-sdk \
-f https://github.com/tetherto/qvac/releases/expanded_assets/sdk-v<version>pip selects the bundled wheel for your platform and pulls the pure-Python deps
(pydantic, bare-rpc, compact-encoding) from PyPI. Then
async with Client() as client: ... just works — the bundled worker is found
automatically, zero configuration. Optional extras: vla (numpy),
notebook (numpy + pandas).
Bundled wheels ship for darwin-arm64, linux-x64, linux-arm64, win32-x64. On any other platform pip falls back to the thin wheel below.
Thin install (any platform)
If there's no bundled wheel for your platform, or you'd rather run a shared system worker, install the thin client from PyPI and provide the worker separately:
pip install tetherto-qvac-sdkThen install the worker once (either route needs Node.js):
# via Python — fetches the exact worker version this package speaks and caches
# it under ~/.cache/qvac/worker/<version>:
python -m tetherto.qvac_sdk install-worker
# or via npm — install the @qvac/sdk version that MATCHES this package (they
# share a version; Client() warns on a mismatch):
npm install -g @qvac/sdk@<your tetherto-qvac-sdk version>Client() then finds the worker automatically (see "Worker resolution" below
for the full lookup order, including pointing QVAC_SDK_DIR at a locally-built
@qvac/sdk for development). The Python route pins the worker to the exact
version this package was generated against, so you never track it yourself.
Upgrading
To upgrade and keep the bundled worker, re-run the install with -f
pointing at the new release tag:
pip install -U tetherto-qvac-sdk \
-f https://github.com/tetherto/qvac/releases/expanded_assets/sdk-v<newversion>A plain pip install -U tetherto-qvac-sdk (no -f, or a stale tag) upgrades to
the thin PyPI wheel: pip applies no priority between locations and just
takes the highest version it can see, which on PyPI is the py3-none-any
wheel — you'd then need install-worker. To switch an already-installed thin
build to the bundled wheel at the same version, add --force-reinstall
(pip treats a version as satisfied regardless of which wheel is installed).
Quickstart
import asyncio
from tetherto.qvac_sdk import Client, load_model, completion, unload_model
from tetherto.qvac_sdk.models import LLAMA_3_2_1B_INST_Q4_0
async def main():
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": "Explain quantum computing in one sentence"}],
)
async for event in run.events:
if event.type == "contentDelta":
print(event.text, end="", flush=True)
print()
await unload_model(t, model_id)
asyncio.run(main())More in examples/ — one runnable example per major capability
(completion events / tools / worker-orchestrated tools, cancel, embeddings,
translation, transcription, TTS, OCR, audio generation, registry queries, model
info, logging, VLA, plugins), mirroring packages/sdk/examples.
The public API (tetherto.qvac_sdk)
Everything you normally need is re-exported flat from tetherto.qvac_sdk; model constants
live in tetherto.qvac_sdk.models.
Client— starts/owns a worker connection;client.transportis passed to every call.- Ergonomic wrappers (kwargs + typed results):
load_model,unload_model,completion,translate,cancel,delete_cache,invoke_plugin/invoke_plugin_stream,model_registry_list/_search/_get_model. (Tool calling iscompletion(tools=...); the worker-orchestrated loop is an advanced path attetherto.qvac_sdk._completion.completion_orchestrate, not flat-public.) - Result types:
CompletionRun(.events,.final),CompletionFinal,ToolCall,TranslateRun. - Generated method stubs for every other contract method (
embed,transcribe,text_to_speech,ocr_stream,audio_gen_stream,diffusion_stream,classify,get_model_info,download_asset, …), each taking a typed request model. - Request/response models + enums (
LoadModelRequest,ModelType, …), also available in full fromtetherto.qvac_sdk.schemas. - Errors (
QvacError,RPCError,InferenceCancelledError,ContextOverflowError, …) forexcept/isinstance. - Logging:
logging_stream,subscribe_server_logs,SDK_LOG_ID,SDK_ALL_LOG_ID. VLA:vla,vla_hparams,vla_preprocess_image,vla_pad_state. - Notebook facade:
tetherto.qvac_sdk.notebook.SyncClient— synchronous, numpy/pandas returns, live in-cell streaming.
The raw generated method stubs are also in tetherto.qvac_sdk.methods, and the pydantic
models in tetherto.qvac_sdk.schemas, if you prefer the explicit modules.
Audio generation currently uses the raw audio_gen_stream stub rather than an
ergonomic Python audio_gen() wrapper. Build an AudioGenStreamRequest, iterate
the progress and base64 PCM frames, and assemble the output audio. See
examples/audiogen.py.
Intentional divergences from @qvac/sdk
Two top-level JS/TS facades are deliberately not ported; Python uses the ecosystem equivalent instead.
getLogger— use the standard libraryloggingmodule. Alogging.Handleris theLogTransportequivalent, and the log-stream surface that has no stdlib counterpart (logging_stream,subscribe_server_logs,SDK_LOG_ID,SDK_ALL_LOG_ID) is already exported above.profiler(process-wide aggregation +exportTable/exportJSON) — use the per-callprofiled_calland the__profilingenvelope helpers intetherto.qvac_sdk.profiling, aggregating the returnedProfilingReports yourself. The global profiler is not a Client-API capability and would only collect useful data after instrumenting the full streaming client.
Notebook / data science
For notebooks and REPLs, tetherto.qvac_sdk.notebook.SyncClient runs the async
client on a background thread so every call is plain and blocking (no await),
with numpy/pandas returns and live in-cell streaming. Needs the notebook
extra (pip install "tetherto-qvac-sdk[notebook]"):
from tetherto.qvac_sdk.notebook import SyncClient
from tetherto.qvac_sdk.models import EMBEDDINGGEMMA_300M_Q4_0
with SyncClient() as client:
m = client.load_model(model_src=EMBEDDINGGEMMA_300M_Q4_0)
vec = client.embed(m, "hello") # 1-D numpy array
df = client.embed_frame(m, ["a", "b", "c"]) # pandas DataFrame, indexed by textembed returns numpy arrays, embed_frame a DataFrame, completion streams
live and returns the text, and transcribe/text_to_speech round-trip audio
as numpy. Full examples: examples/notebook.ipynb
(Jupyter) and examples/notebook.py (script).
Configuring the SDK
Client(config=...) (and BareRpcTransport(config=...)) sends an SDK
config to the worker on connect — the same QvacConfig the TypeScript client
applies (cacheDirectory, loggerLevel, swarmRelays, per-device plugin
defaults, …):
async with Client(config={"cacheDirectory": "/data/qvac-models", "loggerLevel": "warn"}) as client:
...cacheDirectory (must be absolute) relocates where models are stored — handy
for a shared/mounted cache. As a shortcut, the QVAC_CACHE_DIR env var sets
cacheDirectory without code (used by CI to point at a warm model cache).
Worker resolution
Client() locates a worker by trying, in order: explicit worker_path /
bare_path (or QVAC_WORKER_PATH / QVAC_BARE_PATH); a self-contained
bundled wheel (tetherto/qvac_sdk/_bundle/, zero config); sdk_dir / QVAC_SDK_DIR
pointing at an installed @qvac/sdk; a worker fetched by python -m tetherto.qvac_sdk install-worker (into ~/.cache/qvac/worker/<version>, overridable with
QVAC_WORKER_HOME); then a global npm install -g @qvac/sdk. The fetched
worker is version-locked to this package.
Staying in sync with @qvac/sdk (drift avoidance)
The rule: generate what can be generated; put un-generatable behaviour in the worker; hand-write per language only what is irreducibly client-side, and never trust it to match — guard it.
- Generated from the one contract. The pydantic models + typed method stubs,
the model-type resolution maps (
_generated/model_type_maps.py), the error-code registries (_generated/error_codes.py), and the pinned SDK version (_generated/sdk_version.py) are all generated from../sdk/contract/**.generate.py --check(and the SDK'scontract:check) fail CI if either side drifts. - Behaviour lives in the worker.
translatesource-language detection and the completion tool-loop (completionOrchestrate) run in the worker, so Python and JS share one implementation instead of two that diverge (they did: Python once usedlingua, JS@qvac/langdetect-text). - Version lock-step. This package's version is the
@qvac/sdkversion it was generated against. - Conformance corpus.
../sdk/e2e/conformance/cases.jsonis run by both a JS runner andtests/test_conformance.py, so the two clients are diffed against the same cases.
The irreducibly-client-side code (typed error classes, numpy marshaling, stream assembly, the notebook facade) is the only hand-written surface mirroring the JS client, and it's covered by the conformance corpus + real-worker tests rather than trusted to match.
Development
python3 -m venv .venv
.venv/bin/pip install -e ".[gen,dev]"
.venv/bin/python3 scripts/generate.py # regenerate from ../sdk/contract
.venv/bin/python3 -m pytest # unit + (with a built worker) real-model e2eFormat, lint, typecheck, and the generation check (all run in CI):
.venv/bin/python3 scripts/generate.py --check
.venv/bin/python3 -m black --check src/tetherto/qvac_sdk scripts/ tests/
.venv/bin/python3 -m ruff check src/tetherto/qvac_sdk scripts/ tests/
.venv/bin/python3 -m mypy -p tetherto.qvac_sdk && .venv/bin/python3 -m mypy scripts testsReal-model tests spawn a worker (packages/sdk built via bun run build, or
QVAC_POC_SDK_DIR) and otherwise skip. generate.py runs black + ruff --fix --select I (with the package config) on its own output, so a fresh
regeneration already passes the checks above.
Stay updated
Never miss a release
New versions, breaking changes and migration notes - straight to your inbox. No spam, unsubscribe anytime.