SMG Python Bindings
This directory contains the Python bindings for SMG (Shepherd Model Gateway), built using maturin and PyO3.
Quick Start
Installation
pip install maturin
cd smg/bindings/python
maturin develop --features vendored-openssl
Usage
The smg serve command launches backend workers and the SMG router in a single command:
# sglang with gRPC (default)
smg serve --backend sglang --model-path /path/to/model --port 8080
# sglang with HTTP
smg serve --backend sglang --model-path /path/to/model --port 8080 --connection-mode http
# vLLM (gRPC only)
smg serve --backend vllm --model /path/to/model --port 8080
# TensorRT-LLM (gRPC only)
smg serve --backend trtllm --model /path/to/model --port 8080
# Multiple workers (data parallel)
smg serve --backend sglang --model-path /path/to/model --port 8080 --dp-size 4
Serve Options
| Option | Default | Description |
|---|---|---|
--backend |
sglang |
Backend to use: sglang, vllm, or trtllm |
--connection-mode |
grpc |
Connection mode: grpc, http, or zmq. trtllm only supports grpc; tokenspeed only zmq; zmq (a same-host direct engine connection) is available for vllm, tokenspeed, and sglang. Over zmq the router waits for the engine's startup handshake, whose READY comes only once the engine has loaded its model: the router's --zmq-startup-timeout-secs (--router-zmq-startup-timeout-secs here; default 3600) bounds how long any one stage of it may stay silent, and must cover the engine's whole init |
--host |
127.0.0.1 |
Host for the router |
--port |
8080 |
Port for the router |
--dp-size |
1 |
Data parallel size (number of worker replicas) |
--worker-host |
127.0.0.1 |
Host for worker processes |
--worker-base-port |
31000 |
Base port for workers |
--worker-startup-timeout |
300 |
Seconds to wait for workers to become healthy |
Backend-specific options (e.g., --tensor-parallel-size, --quantization) are passed through to the backend.
Secrets from the environment or files
The serving keys and the store URLs need not be on the command line, where other processes can read them (/proc/<pid>/cmdline, process listings, a Deployment's args). Each is also read from an environment variable, or from a file such as a mounted Secret, by the Python launcher (smg launch, smg serve with the --router- prefix) and by the Rust binary alike:
| Setting | Flag | Environment | File flag | File environment |
|---|---|---|---|---|
| Shared API key | --api-key |
SMG_API_KEY |
--api-key-file |
SMG_API_KEY_FILE |
| Per-tenant API keys | --tenant-api-key (repeatable) |
SMG_TENANT_API_KEYS |
--tenant-api-keys-file |
SMG_TENANT_API_KEYS_FILE |
| PostgreSQL URL | --postgres-db-url |
SMG_POSTGRES_DB_URL |
--postgres-db-url-file |
SMG_POSTGRES_DB_URL_FILE |
| Redis URL | --redis-url |
SMG_REDIS_URL |
--redis-url-file |
SMG_REDIS_URL_FILE |
| Oracle credentials | --oracle-connect-descriptor, --oracle-username, --oracle-password |
ATP_DSN, ATP_USER, ATP_PASSWORD |
– | – |
- The flag beats the environment. A value (from the flag or the environment) together with a file for the same secret is refused at start-up rather than ranked: the second source of one secret is a stale one about to be honoured.
- A single-value file holds the value alone; the surrounding whitespace (a trailing newline) is dropped, and so is the whitespace around a single value from the environment (a Secret made from a file carries the editor's newline into both); the
ATP_*values are the exception, see below. An empty file, or one with more than one line, is refused. SMG_TENANT_API_KEYSholdstenant_id:keyentries separated by,or a newline; the file holds one entry per line, blank lines and lines starting with#skipped. A source that is set but holds no entry is refused.- A store URL file is read for the selected
--history-backendalone. The launcher still reads the olderPOSTGRES_DB_URLandREDIS_URLnames when theSMG_names are unset. - The Oracle credentials have no file source; the launcher and the binary (
--oracle-dsn,--oracle-user,--oracle-password) read theATP_*variables when the flags are absent, as they always did, and use them exactly as set, with no trimming: a Secret for them is made with--from-literal, or from a file that ends without a newline (a password that ends in one is a wrong password to the database). - No value is echoed: a refusal names the sources and the path, and
--helpshows no value of a variable. The start-up record (CREDENTIAL SOURCES) names each secret's source asflag,envorfile; a launcher-started gateway writes the same record, withlauncherfor a key, or the selected history backend's store URL, that the launcher gave as a value (the binding does not see whether a flag or the environment gave it) andfilefor a key read from a file.
Tokenizer
smg.tokenizer.Tokenizer is the tokenizer SMG serves with, for Python: the same Rust code (llm-tokenizer) loads the checkpoint, encodes and decodes, and chats render through the gateway's own request path, so a training or evaluation pipeline gets the token ids SMG feeds the model without running a gateway.
from smg.tokenizer import Tokenizer
# What the gateway loads, and the id it serves it as.
tokenizer = Tokenizer("/models/Qwen3-8B", model="Qwen/Qwen3-8B")
ids = tokenizer.encode("hello world") # list[int]
batch = tokenizer.encode_batch(["hello world", "how are you?"]) # parallel, on a Rust thread pool
text = tokenizer.decode(ids)
messages = [{"role": "user", "content": "hi"}, {"role": "assistant", "content": "hello!"}]
# The ids SMG would feed the model for this conversation.
ids = tokenizer.apply_chat_template(messages)
prompt = tokenizer.apply_chat_template(messages[:1], add_generation_prompt=True, tokenize=False)
weather = {"type": "object", "properties": {"city": {"type": "string"}}}
tools = [{"type": "function", "function": {"name": "get_weather", "parameters": weather}}]
request = {"messages": messages[:1], "tools": tools, "reasoning_effort": "low"}
rendered = tokenizer.render_chat(request)
rendered["token_ids"], rendered["text"], rendered["from_text"], rendered["warnings"]
apply_chat_templatetakestransformers' shape (a conversation or a list of them,tools,add_generation_prompt=False,continue_final_message,tokenize=True, extra keyword arguments for the template) and builds the/v1/chat/completionsrequest a client would send;render_chattakes that request body itself (a mapping or its JSON text, so logged bodies replay as they are;render_chat_batchrenders a list of them in parallel) and returns the prompt text, its ids, how they were rendered and the rewrites made on the way. Both go through serving's steps: the request deserialized, normalized and validated as the HTTP layer does and refused where the gRPC router refuses it (a refused request raisesValueErrorwith the gateway's reason; a tool call needs itsidandtype), message content shaped for the template, tool-callargumentsstrings parsed into objects for templates that serialize them,tool_choice,reasoning_effortandchat_template_kwargshanded to the template as the gateway hands them, then the prompt encoded as the gateway does: the text without special tokens, or the renderer's own encode for renderers that encode segment by segment (Kimi K3; DeepSeek V3.2 / V4 / V4.1 when message text spells a control token), wherefrom_textis false and no encode of the text gives the ids.- A request renders under
Tokenizer.model, as the router renders it under the served model's id (a request naming another model, a client's alias, is rewritten to it): the id selects the provider profile (provider_profile: DeepSeek, Kimi, MiniMax, GLM) whose request rewrites apply. It defaults to the Hub id of a Hub source or of a Hugging Face cache snapshot (.../models--{org}--{name}/snapshots/...); for any other path, passmodel=the id SMG serves the checkpoint as, or a vendor-named alias of it (the gateway's--model-aliasregistry is not read). Without one, a request renders under the model it names itself, which selects a profile by that name alone (a logged body carries the client's id, perhaps an alias: passmodel=when replaying them), and a request that names none, as everyapply_chat_templatecall, raises. - Parity holds against a gateway built from the same smg commit (
Tokenizer.smg_build; compare the gateway's--version-verbose) that loads the same files: pass what the gateway loads (its tokenizer path, else its model path;config.jsonandgeneration_config.jsonbeside the tokenizer files select the renderer and the stop ids,eos_token_ids), andchat_template=the file the gateway's--chat-templategets.Tokenizer.fingerprintdigests the build, the model id, the chat template file, a file source itself and the checkpoint's tokenizer files (those the gateway's Hub download fetches, withconfig.jsonandgeneration_config.json, and DeepSeek V4.1'sencoding/encoding.py, which that download does not fetch): tokenizers with one fingerprint render and encode alike. apply_chat_templateraises wheretransformerswould do something SMG does not: a keyword argument naming a variable the render fills itself (strftime_nowor a special token the checkpoint defines, whichtransformerslets the argument replace, ormessages) isTypeError, and so are atransformersargument SMG has no equivalent for (chat_template,documents,padding,truncation,max_length,return_tensors,return_dict,return_assistant_tokens_mask,tokenizer_kwargs,sanitize_control_tokens) not given its no-op value and acontinue_final_messagenaming a field other thancontent; a rendering that does not honouradd_generation_promptorcontinue_final_message(a renderer whose own encoder writes the generation header after a final tool or user message; a continued message that is not the assistant's) isValueError, whilerender_chatreturns such a rendering as SMG makes it. A final assistant message the renderer cannot continue itself is continued as SMG continues it, its text appended after the generation header (generation_prompttrue,continued_final_messagefalse). The rewrites serving only logs (tool-callargumentsthat are not a JSON object rendered as{}, another provider's message extensions dropped) areRenderedChat["warnings"], andapply_chat_templateissues them assmg.tokenizer.RenderWarning.encodeadds the post-processor's special tokens by default (add_special_tokens=True), astransformersdoes; SMG encodes/v1/completions,/generateand/v1/tokenizetext withadd_special_tokens=Falseand embedding input withTrue. Special-token text in the input encodes to the special token.decodetakes a list, a numpy array or a torch tensor of ids, and does not applytransformers'clean_up_tokenization_spaces. A legacy-format Llama-classtokenizer.json(aPrependnormalizer) is encoded astransformers'LlamaTokenizerclass builds it, the pipeline the engines load, which can differ fromtransformers4.x in leading-space pieces.- Short calls (an encode of less than 16 KiB, a decode of fewer than 4096 ids, a chat whose request and prompt are shorter than 16 KiB) run on the calling thread with the GIL held, as Hugging Face's
tokenizersruns a single encode: releasing it would cost more than the work, since a thread that releases the GIL takes it back only when the thread running Python meanwhile gives it up (up to the 5 ms switch interval later). Encode many texts withencode_batch. The batch methods and longer calls release the GIL. A decode of 4096 ids or more, the parse, checks and render of a chat request of 16 KiB or more, a Hub download, and reading and hashing the loaded files forTokenizer.fingerprintstay on the calling thread; what can reach rayon (the batch methods,apply_chat_templategiven several conversations, building the tokenizer, and any encode of 16 KiB or more, a chat's prompt among them) runs on the binding's own Rust thread pool, one per process, ofSMG_TOKENIZER_THREADSthreads (one whenTOKENIZERS_PARALLELISMis off, rayon's default otherwise:RAYON_NUM_THREADS, else the CPU count); each worker of a multi-process pipeline builds its own on first use, so size it for the workers it runs beside. A process forked after the parent used the tokenizer (torchDataLoaderworkers,datasets.map(num_proc=...)) builds a pool of its own, where rayon's global pool would leave it hanging, andos.fork()waits for the tokenizer calls running in other threads (not for a Hub download, which holds none of their locks; a fork made from C waits for none). A tokenizer pickles as the files it loaded (a Hub id's snapshot), the smg build and the fingerprint, and reloads from those files in the receiving process, from the cache on the same host; where the path it loaded is missing (another host), from a download of the Hub snapshot it was in. Unpickling refuses another smg build, and files whose fingerprint changed (edited in place, only partly downloaded into this host's cache, or the Hub'smainmoved on since; a DeepSeek V4.1 tokenizer loaded withencoding/encoding.pyreloads only where that file is too, since the Hub download does not fetch it); and because the fingerprint is in the pickle,datasets.maprecomputes after a template or tokenizer file changes. - Hub model ids download as the gateway downloads them (
HF_TOKEN,HF_HOMEandHF_ENDPOINTare honoured, the revision is alwaysmain); for a pinned revision or an offline host, download withhuggingface_hub.snapshot_downloadand pass the directory. - Not reproduced: media content (refused: SMG expands each image, video or audio item into the model's own tokens), gpt-oss models (refused: SMG renders them with Harmony), and the date a template writes with
strftime_now, which is the local time at the render, as in serving (the samellm-tokenizercode). WhereSOURCE_DATE_EPOCH(whole seconds since the Unix epoch) is set when the process loads its first chat template, the template writes that instant instead, also in local time (setTZ=UTC0too for the same text on every Linux or macOS host; on Windows the system time zone applies):llm-tokenizerreads the variable once, then, so set it in the environment the process starts with (a worker forked after its parent loaded one keeps the parent's reading). - Importing the tokenizer loads the whole
smgextension: its jemalloc settings apply to the Rust side's allocations (_RJEM_MALLOC_CONFoverrides them), and the wheel's dependencies (grpcio,grpcio-health-checking,setproctitle,PyYAML) come with it.
Directory Structure
bindings/python/
├── src/ # Source code (src layout)
│ ├── lib.rs # Rust/PyO3 bindings implementation
│ ├── tokenizer.rs # The tokenizer binding (Rust half)
│ └── smg/ # Python source code
│ ├── __init__.py
│ ├── cli.py # CLI entry point
│ ├── serve.py # smg serve implementation
│ ├── launch_router.py
│ ├── router.py
│ ├── router_args.py
│ └── tokenizer.py # smg.tokenizer.Tokenizer
├── tests/ # Python unit tests
├── Cargo.toml # Rust package configuration
├── pyproject.toml # Python package configuration
└── README.md # This file
Building
Development Build
pip install maturin
cd smg/bindings/python
maturin develop --features vendored-openssl
Production Build
cd smg/bindings/python
maturin build --release --out dist --features vendored-openssl
pip install dist/smg-*.whl
Testing
cd smg/bindings/python
pytest tests/
Adding a router flag
A flag lives in a few places, and the tests pin them against each other:
model_gateway/src/main.rs: the clap flag, in its group ofCliArgs. The Rust CLI is the reference:tests/test_cli_parity.pyfails on a flag that exists on one side only.src/smg/router_args.py: aRouterArgsfield, and its option inadd_cli_argsunder the matching help group. The field goes at its alphabetical place among the keyword-only fields (every field afterworker_urls,host,port), never at the tail:tests/test_arg_parser.pypins the order and says where a misplaced field belongs.src/lib.rs: theRouterstruct field, the#[pyo3(signature)]entry with the flag's default, the parameter ofnewand the field of the literalnewbuilds, each at the same alphabetical place among the keyword-only parameters (every parameter afterworker_urls,policy,host,port; pyo3 wants the signature and the parameters in one order), plus the line that hands the value to the router configuration into_router_config.tests/test_router_config.pypins the order;tests/test_router_args_parity.pyfails on a dataclass field the constructor does not take, and on a parameter the launcher never sets.- The flag's own tests go next to the related ones, or in a file of their own, not at the end of
tests/test_arg_parser.py, where every flag's tests would meet.
The keyword-only parameters have no position, so two pull requests that each add a flag change different lines of each file and merge without a conflict.
Metadata
Release files for smg 1.15.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| smg-1.15.0.tar.gz | 11.2 MB | Without provenance | Details for smg-1.15.0.tar.gz |
Built distributions (wheels)
Total release size: 374.6 MB
Release files / smg-1.15.0.tar.gz
| Download URL | smg-1.15.0.tar.gz |
|---|---|
| Size | 11.2 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
dd4cee186dfb446bc931fd3ce2350386dc62fd98de18b6af4f0e13a1f28f1e8b
|
|
BLAKE2b-256 checksum How to use checksums |
ec5bcfbdc3ac7993364f00af6fdf703be38739cb8948950b324fc753adcf1c15
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.18
|
Release files / smg-1.15.0-cp38-abi3-win_amd64.whl
| Download URL | smg-1.15.0-cp38-abi3-win_amd64.whl |
|---|---|
| Size | 52.7 MB |
| Tags | CPython 3.8 Windows x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
554b37b82dc08c4d56ecef4d228f3f6327b91b8c8a394039879377cc0cb4a1f5
|
|
BLAKE2b-256 checksum How to use checksums |
251fc0f909be0f24dfff2d4f8f04df6dd4385fe9957e90d5baf1f920190871e1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.18
|
Release files / smg-1.15.0-cp38-abi3-musllinux_1_1_x86_64.whl
| Download URL | smg-1.15.0-cp38-abi3-musllinux_1_1_x86_64.whl |
|---|---|
| Size | 53.2 MB |
| Tags | CPython 3.8 Linux musl 1.1+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
32d0d28568f811b1a6a2f7a4407b71c48681ccbb8c2b86785878a0cf041f8cce
|
|
BLAKE2b-256 checksum How to use checksums |
2ca50d7ec7627ce5859f574f4f7e445636803a2bffeab02c0eff460cd824e4a6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.18
|
Release files / smg-1.15.0-cp38-abi3-musllinux_1_1_aarch64.whl
| Download URL | smg-1.15.0-cp38-abi3-musllinux_1_1_aarch64.whl |
|---|---|
| Size | 50.5 MB |
| Tags | CPython 3.8 Linux musl 1.1+ ARM64 abi3 |
|
SHA-256 checksum How to use checksums |
1aa9af6d183c5d0cd002c054cdb6a8a8524a38651ba193c2c8ddfa87a3887555
|
|
BLAKE2b-256 checksum How to use checksums |
66c9ea634ae442cbfffdbc4e8d531e3435b7dce7d24a86cbc9b811ee996df6b3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.18
|
Release files / smg-1.15.0-cp38-abi3-manylinux_2_28_aarch64.whl
| Download URL | smg-1.15.0-cp38-abi3-manylinux_2_28_aarch64.whl |
|---|---|
| Size | 52.6 MB |
| Tags | CPython 3.8 Linux glibc 2.28+ ARM64 abi3 |
|
SHA-256 checksum How to use checksums |
3e4ba779bf055d31f66b30d777a3e7f1d2ab1a80b0ca41a42745a40b6572eaee
|
|
BLAKE2b-256 checksum How to use checksums |
b885ca4938c1b69da81b775237fb6b8b5746420f37751695ef491e5e5ebb47b0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.18
|
Release files / smg-1.15.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl
| Download URL | smg-1.15.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl |
|---|---|
| Size | 55.2 MB |
| Tags | CPython 3.8 Linux glibc 2.17+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
1f269c39fa06e09f6fce1e045c73a8450261377f5a36f22acdab6abfc28b175b
|
|
BLAKE2b-256 checksum How to use checksums |
0c98921a1b241f6ccda1d4ac3617a4ce0997f25e387a67964010865a3663333b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.18
|
Release files / smg-1.15.0-cp38-abi3-macosx_11_0_arm64.whl
| Download URL | smg-1.15.0-cp38-abi3-macosx_11_0_arm64.whl |
|---|---|
| Size | 48.3 MB |
| Tags | CPython 3.8 abi3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
7e6f672d1695f5955fa24d729d2ca4c217548409163f6e6cf3bff49bc81a7600
|
|
BLAKE2b-256 checksum How to use checksums |
f01df111da63faad6b919cec0ee95fff8ad085e9e28cb142bd00046a5974f832
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.18
|
Release files / smg-1.15.0-cp38-abi3-macosx_10_12_x86_64.whl
| Download URL | smg-1.15.0-cp38-abi3-macosx_10_12_x86_64.whl |
|---|---|
| Size | 51.0 MB |
| Tags | CPython 3.8 abi3 macOS 10.12+ x86-64 |
|
SHA-256 checksum How to use checksums |
f9a7ec3553df53e335badf0d14297ac79fcad04ec36258a2c06c1aeed078c8df
|
|
BLAKE2b-256 checksum How to use checksums |
5313e527fe2f3cf100a365b35330a37bda34048b4b95fa41077a5831065b2b20
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.18
|
Security for smg 1.15.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.