QVAC SDK v0.20.0
📦 NPM: https://www.npmjs.com/package/@qvac/sdk/v/0.20.0
QVAC SDK 0.20.0 adds TranslatePsy-AfriSLM translation, an in-process TurboVec vector index, MiniMax-H3 video, Parakeet Nemotron transcription, and the rest of the AudioGen and TTS surfaces. ABot-World sessions (worldCreateScene / worldStep) are on this surface. loadModel runs an advisory llama.cpp fit check. Diffusion VAE constant names, CPU flags, 'row' split, Parakeet language codes, and how system prompts combine with KV cache all change.
@qvac/sdk, @qvac/inference, and tetherto-qvac-sdk all ship at 0.20.0. Install @qvac/sdk and @qvac/inference together at this version.
Table of Contents
Breaking Changes
Diffusion CPU flags
clip_on_cpu, vae_on_cpu, and control_net_cpu are gone. Put the text encoder, VAE, or ControlNet on CPU through params_backend and backend. Layer streaming still needs CPU diffusion parameter residency and max_vram.
Before:
const modelConfig = {
clip_on_cpu: true,
vae_on_cpu: true,
control_net_cpu: true
}After:
const modelConfig = {
params_backend: 'te=cpu,vae=cpu',
backend: 'controlnet=cpu'
}CPU layer streaming:
const modelConfig = {
params_backend: 'diffusion=cpu',
max_vram: -1,
stream_layers: true
}Diffusion VAE constant names
Generated VAE exports now include the registry type tag, so LTX audio/video and ABot TAeHV/Wan no longer collapse to _1 suffixes.
Before:
ABOT_WORLD_0_5B_LF_VAE
ABOT_WORLD_0_5B_LF_VAE_F16
LTX_2_3_VAE
LTX_2_3_VAE_1After:
ABOT_WORLD_0_5B_LF_TAEHV_VAE
ABOT_WORLD_0_5B_LF_WAN_VAE
LTX_2_3_AUDIO_VAE
LTX_2_3_VIDEO_VAESystem prompts apply on every completion path
A system message in history is no longer dropped when kvCache is on. modelConfig.system_prompt is applied when the history has no system message, including turns that do not use KV cache.
LLM_CONFIG_DEFAULTS.system_prompt is 'You are a helpful assistant.'. loadModel without an explicit system_prompt still carries that default, so a user-only history now reaches the model with a system message.
Before:
completion({
modelId, // loaded with system_prompt: 'You are a helpful assistant.'
history: [
{ role: 'system', content: 'Always answer with the word BANANA.' },
{ role: 'user', content: 'What is the capital of France?' }
],
kvCache: true
})
// the caller's system message is discardedAfter:
completion({
modelId, // loaded with system_prompt: 'You are a helpful assistant.'
history: [
{ role: 'system', content: 'Always answer with the word BANANA.' },
{ role: 'user', content: 'What is the capital of France?' }
],
kvCache: true
})
// the caller's system message is honouredCallers that previously sent user-only history and relied on no system message must pass system_prompt: '' at load (or an explicit empty system turn) if they need that behaviour.
'row' split is rejected
Fabric 10549.1.0 dropped llama.cpp's unused row split.
Completion models: modelConfig['split-mode'] is 'none', 'layer' or 'tensor'. 'row' is rejected.
Embedding models: modelConfig.splitMode is 'none' or 'layer'. 'row' is rejected.
'tensor' on completion models is unaffected.
On embeddings, 'row' did not do tensor parallelism; it already behaved as 'layer'. Embeddings have no 'tensor' mode. Use 'layer'.
Before:
// completion
modelConfig: { 'split-mode': 'row' }
// embedding
modelConfig: { splitMode: 'row' }After:
// completion
modelConfig: { 'split-mode': 'layer' } // or 'tensor'
// embedding
modelConfig: { splitMode: 'layer' }Parakeet language is a locale code
Parakeet modelConfig.language must be auto, a short code (en, hi), or code-region (en-US, hi-IN). zh-Hans-CN and names like english now fail at loadModel. Whisper language is unchanged.
Before:
modelConfig: {
language: 'zh-Hans-CN'
}After:
modelConfig: {
language: 'zh'
}New APIs
TurboVec vector index
createVectorIndex builds an in-worker vector index on the embedding plugin's TurboVec engine. Pair it with embed(): add each document's embedding under an id you choose, search with a query embedding, and map the returned ids back to your store. Default storage is VectorIndexStorage.TURBOVEC_Q4. Dimension must be a multiple of 8 and at most 1024 for TurboVec modes.
import { createVectorIndex, loadVectorIndex, VectorIndexStorage } from '@qvac/sdk'
const index = await createVectorIndex({
dim: 1024,
storage: VectorIndexStorage.TURBOVEC_Q4
})
await index.add({ ids: ['1', '2'], vectors: embeddings })
const hits = await index.search({ query: queryEmbedding, k: 3 })
await index.write({ path: 'indexes/articles.qvi' })
await index.dispose()
const reopened = await loadVectorIndex({ path: 'indexes/articles.qvi' })MiniMax-H3 video
video() can run MiniMax-H3 in txt2vid mode. Load sdcpp-generation with mode: 'video' and the H3 text-encoder, video VAE, and audio VAE sources.
const modelId = await loadModel({
modelType: 'sdcpp-generation',
modelSrc: '/models/h3.gguf',
modelConfig: {
mode: 'video',
llmModelSrc: '/models/h3-text-encoder.gguf',
vaeModelSrc: '/models/video-vae.safetensors',
audioVaeModelSrc: '/models/audio-vae.safetensors',
backend: 'vulkan0',
stream_layers: false
}
})
const result = video({
modelId,
mode: 'txt2vid',
prompt: 'Steam rises from coffee.',
video_frames: 124,
fps: 24,
cfg_scale: 1
})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_TAEHV_VAE,
t5XxlModelSrc: UMT5_XXL_ENC_Q8_0,
vaeModelSrc: ABOT_WORLD_0_5B_LF_WAN_VAE,
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.
Parakeet Nemotron
Three Parakeet Nemotron 0.6B weights are on the SDK catalog for parakeet-transcription.
import { loadModel, PARAKEET_NEMOTRON_0_6B_Q4_0 } from '@qvac/sdk'
const modelId = await loadModel({
modelSrc: PARAKEET_NEMOTRON_0_6B_Q4_0,
modelType: 'parakeet-transcription',
modelConfig: {
language: 'auto',
streaming: true
}
})AudioGen understand, remake, and edit
audioUnderstand returns caption, bpm, keyscale, and audio codes. Pass those codes back into audioGen to re-synthesize the clip, optionally with LRC lyrics. audioEdit runs an ordered flow-edit / repaint pipeline on a source recording and returns the same run shape as audioGen.
const run = audioUnderstand({ modelId, sourceAudio: '/path/to/song.wav' })
const { caption, bpm, keyscale, audioCodes } = await run.description
const remake = audioGen({ modelId, caption, audioCodes, generateLrc: true })
const { lrc, lyricsScore } = (await remake.stats) ?? {}
const edited = audioEdit({
modelId,
sourceAudio: '/path/to/song.wav',
operations: [
{ type: 'flow-edit', from: { caption: 'acoustic folk' }, to: { caption: 'synthwave' } },
{ type: 'repaint', caption: 'analog synth solo', start: 10, end: 20 }
]
})TTS load options, sample rate, and cancel
Load-time CosyVoice3, Chatterbox, Supertonic, and Parler fields that were previously unreachable now pass through. textToSpeech exposes sampleRate on the first frame, real stats (realTimeFactor, backendId, generatedFrames), and cancel({ requestId }) stops the engine, not only delivery.
const result = textToSpeech({ modelId, text })
const sampleRate = await result.sampleRate
for await (const sample of result.bufferStream) play(sample)
await cancel({ requestId: result.requestId })
const stopReason = await result.stopReason // 'cancelled' when the engine stoppedTool grammar
Completions with tools consume llama.cpp 0.53.0 grammar. generationParams.tool_choice: 'required' forces a tool call. When the model emits a tool region that does not parse, final.toolErrors lists the failures; the field is omitted when there are none.
const run = completion({
modelId,
history,
tools: [getWeather],
stream: false,
generationParams: { tool_choice: 'required' }
})
const final = await run.final
if (final.toolCalls.length === 0) console.log(final.toolErrors)Features
translate() detects TranslatePsy-AfriSLM by registry name or GGUF filename and applies that family's prompts and deterministic decoding. Custom context is ignored for these models.
Before each llama.cpp completion or embedding load, loadModel runs @qvac/model-fit in a disposable child and logs fit / does-not-fit / no evidence. The check is fail-open: a missing verdict, crash, timeout, or refusal never blocks the load. Set QVAC_ADVISORY_MODEL_FIT=0 to skip it. This is separate from assessModelFit and from the in-process mobile fit below.
On Android and iOS, llama assessModelFit runs @qvac/model-fit in-process on a worker thread. There is no disposable child process on those hosts. A leftover .running marker from a previous abort is treated as crashed so the same path and config skip native instead of retrying the abort. The JavaScript loop stays free while the fit runs.
@qvac/tts-ggml 0.9.x installs host binaries in per-platform packages (@qvac/tts-ggml-darwin-arm64 and siblings) next to the meta package. qvac verify bundle looks there instead of under the meta package's prebuilds/.
Bug Fixes
Automatic KV-cache files keep the last committed saved-message boundary across a worker restart, so a warm turn does not replay already-cached messages. A cancelled or failed warm turn no longer deletes that committed file.
An addon's logger is attached when its model loads, not when the plugin registers, so unused addons do not open log sinks.
Mobile withQvacSDK prebuild verifies only the hosts the bundle actually links, and only the current Android or iOS target's hosts. A missing addon pin now names the exact platform package instead of a generic missing-prebuild error.
Model Changes
Added
ABOT_WORLD_0_5B_LF_TAEHV_VAE
ABOT_WORLD_0_5B_LF_WAN_VAE
LTX_2_3_AUDIO_VAE
LTX_2_3_VIDEO_VAE
PARAKEET_0_6B_F16
PARAKEET_0_6B_Q4_0
PARAKEET_0_6B_Q8_0
PARAKEET_NEMOTRON_0_6B_F16
PARAKEET_NEMOTRON_0_6B_Q4_0
PARAKEET_NEMOTRON_0_6B_Q8_0
TRANSLATEPSY_AFRISLM_0_8B_TRANSLATION_Q4_K_M
TRANSLATEPSY_AFRISLM_0_8B_TRANSLATION_Q8_0
TRANSLATEPSY_AFRISLM_2B_TRANSLATION_Q4_K_M
TRANSLATEPSY_AFRISLM_2B_TRANSLATION_Q8_0
TRANSLATEPSY_AFRISLM_4B_TRANSLATION_Q4_K_M
TRANSLATEPSY_AFRISLM_4B_TRANSLATION_Q8_0Removed
ABOT_WORLD_0_5B_LF_VAE
ABOT_WORLD_0_5B_LF_VAE_F16
LTX_2_3_VAE
LTX_2_3_VAE_1The 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,audio_edit_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. Source-driven editing
(audio_edit_stream with an AudioEditStreamRequest carrying the ordered
flow-edit / repaint operations) streams the same frame shape.
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.