Using a Server#
Once a server is running (see Serving), you can reach it three ways: the native
/generate endpoint, the Python SDK, or the OpenAI-compatible API. Every model is
reachable via /generate and the SDK; the OpenAI routes cover the chat, speech, and
image models.
Native /generate#
POST /generate takes a multipart form and returns either a single JSON document or an
NDJSON stream.
Field |
Default |
Meaning |
|---|---|---|
|
— |
Text prompt (optional if media is provided). |
|
— |
One or more media uploads; each file’s modality is inferred from its extension. |
|
auto |
Comma-separated input modalities, one entry per prompt element in order. Auto-detected from the uploads and text when omitted, which is what keeps the ordering and the count of same-modality attachments; an explicit list replaces it. |
|
|
Comma-separated desired outputs (e.g. |
|
|
|
|
— |
JSON object of model-specific parameters (e.g. |
|
(uuid) |
Optional client-supplied id; the server generates one when omitted. |
A non-streaming response groups outputs by modality, each payload base64-encoded:
{
"request_id": "…",
"outputs": {
"text": [{"data": "<base64>", "metadata": {}}],
"image": [{"data": "<base64-png>", "metadata": {}}]
}
}
A streaming response is application/x-ndjson — one JSON object per line as chunks
arrive. A client that sends Accept: application/vnd.mstar.frames receives
length-framed binary chunks instead, which skips the base64 pass on multi-megabyte
video_frame payloads; a server without the framing keeps answering NDJSON, so a
client must parse by the response Content-Type. GET /health returns
{"status": "healthy"}.
# text (non-streaming → JSON)
curl -s http://localhost:8000/generate -F 'text=Hello' -F 'streaming=false'
# image understanding (image in, text out)
curl -s http://localhost:8000/generate -F 'text=What is in this image?' -F 'files=@cat.jpg'
# text-to-speech (base64 PCM in outputs.audio)
curl -s http://localhost:8000/generate \
-F 'text=hello there' -F 'output_modalities=audio' \
-F 'model_kwargs={"voice":"tara"}' -F 'streaming=false'
WebSocket /generate/ws#
/generate/ws is the same request over one persistent WebSocket, for control loops
that cannot afford an HTTP round trip per step (robot policies, streaming world models).
Each message is one request with the /generate fields — text, files as
[{"name": ..., "data": ...}], input_modalities, output_modalities,
model_kwargs, request_id — sent either as a JSON text frame (data base64) or
as a msgpack binary frame (data raw bytes). Replies use the same encoding as the
message: one frame per result chunk, {"request_id", "modality", "data", "metadata"},
then {"request_id", "finish": true}. A rejected message answers
{"request_id", "error": ...} and the socket stays open; a request that fails after it
was accepted ends the same way, with the HTTP status it would have had, and no
finish follows. Messages may be pipelined —
send the next observation before the current action chunk has returned — and the
request_id tells the replies apart. Closing the socket aborts whatever is still in
flight.
import msgpack, numpy as np, websockets.sync.client
with websockets.sync.client.connect("ws://localhost:8000/generate/ws", max_size=None) as ws:
ws.send(msgpack.packb({
"text": "pick up the mug",
"files": [{"name": "obs.jpg", "data": open("obs.jpg", "rb").read()}],
"output_modalities": ["action"],
"model_kwargs": {"action_mode": "policy", "domain_name": "droid_lerobot",
"raw_action_dim": 10, "action_chunk_size": 32},
"request_id": "step-0",
}, use_bin_type=True))
while True:
reply = msgpack.unpackb(ws.recv(), raw=False)
if reply.get("modality") == "action":
actions = np.frombuffer(reply["data"], dtype=np.float32).reshape(32, -1)
if reply.get("finish") or reply.get("error"):
break
examples/cosmos3_action_ws_client.py is a complete openpi-style client that runs this
loop at a fixed observation rate and reports chunks/s, actions/s and latency percentiles.
Python SDK#
The SDK (mstar.client.MStarClient) is a thin HTTP client over /generate. It
depends only on requests (plus numpy for the audio helpers) — no torch — so it can
run anywhere:
from mstar import MStarClient
client = MStarClient("http://localhost:8000") # optional: timeout=600.0
The core method is generate:
generate(*, text=None, images=None, audio=None, video=None, output_modalities=("text",), input_modalities=None, stream=False, request_id=None, **model_kwargs)Submit a request.
images/audio/videoaccept a path, rawbytes, a(filename, bytes)tuple, or a list of those. Extra keyword args are forwarded as the model’smodel_kwargs(e.g.voice="tara",temperature=0.7,max_output_tokens=256);Nonevalues are dropped. Returns aGenerateResultwhenstream=False, or an iterator of stream events whenstream=True.
Convenience wrappers:
Method |
Returns |
|---|---|
|
Text generation (and, with |
|
PNG |
|
PNG |
|
An |
|
The |
|
Sugar for |
|
|
Result and event types live in mstar.client:
GenerateResult—.text,.images(list of PNG bytes),.audio(anAudioBufferorNone),.raw; plus.save_image(path)/.save_audio(path).AudioBuffer— decoded PCM with.sample_rate;.to_wav(path),.to_numpy(),len(...).Stream events —
TextChunk(text),ImageChunk(data)(.save(path)),AudioChunk(pcm, sample_rate), andVideoFrameChunk(data, metadata). A video-frame chunk validates its width, height, fps, pixel format and frame range;.to_numpy()returns a zero-copy[frame_count, height, width, 3]uint8 view. Rawvideo_framerequests requirestream=True. The SDK asks for the binary framing automatically forvideo_frame; other modalities opt in withMStarClient(prefer_binary=True). The server does not pace generation to the consumer: frames are produced at model speed and buffered by the API server until read, bounded by the request’s frame count, so a consumer slower than realtime accumulates that backlog in server memory (about 11 MiB per chunk at 720p).
res = client.chat("Hello!") # GenerateResult
print(res.text)
open("cat.png", "wb").write(client.generate_image("a cat in a hat"))
client.tts("Hi there", voice="tara").to_wav("out.wav")
for event in client.stream(text="Tell me a story"):
print(getattr(event, "text", ""), end="", flush=True)
OpenAI-compatible API#
mstar mounts OpenAI-style routes under /v1 for the models with standard OpenAI
semantics. Point any OpenAI client at http://<host>:<port>/v1:
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8000/v1", api_key="none")
Endpoints and model coverage:
Endpoint |
Models |
Notes |
|---|---|---|
|
all |
Lists the served model. |
|
|
Text chat (streaming + non-streaming). Qwen3-Omni can also emit speech. |
|
|
Text-to-speech ( |
|
speech models that publish a voice list through their speech adapter ( |
The |
|
|
Speech-to-text (multipart upload; |
|
models whose adapter can continue a hypothesis |
Streaming speech-to-text: |
|
|
Text-to-image. |
|
|
Image editing (image + prompt → image). |
Models without an OpenAI surface (pi05, vjepa2, vjepa2_ac, waypoint)
return 404 on /v1/*; use /generate or the SDK for them. In particular,
Waypoint emits live RGB frame chunks and is not routed through the encoded-video
/v1/videos/generations endpoint.
# chat
client.chat.completions.create(model="bagel", messages=[{"role": "user", "content": "hi"}])
# text-to-speech
client.audio.speech.create(model="orpheus", input="hello there", voice="tara")
# (mstar extension) the voices the served model accepts
requests.get("http://localhost:8000/v1/audio/voices").json()["voices"]
# speech-to-text
client.audio.transcriptions.create(model="whisper_large", file=open("speech.wav", "rb"),
language="en")
# image generation
client.images.generate(model="bagel", prompt="a cat in a hat")
Per-model notes:
BAGEL — chat returns text only; use
/v1/images/generationsand/v1/images/editsfor image output.Qwen3-Omni — text sampling uses
thinker_*keys, speech usestalker_*, and the residual codec groups usecode_predictor_*; set the speaker withvoice(defaultEthan) and request audio output by including"audio"inmodalities. Non-OpenAI knobs (e.g.talker_top_k,code_predictor_top_p) go throughextra_body.Whisper / Higgs-Audio —
language(ISO-639-1) skips language detection,promptconditions the decoder on prior text (it reaches the model asinitial_prompt), andresponse_formatverbose_json/srt/vtt(ortimestamp_granularities[]) asks the model for timestamps. Whisper’s language and timestamp tokens travel in the text stream and are lifted intolanguage/segmentsby the server; a streaming client receives only the spoken words. Uploads longer than the model’s clip (30 s for Whisper) are served as consecutive windows. By default they run in order, openai-whisper style: each window gets the transcript so far asinitial_promptand the first window’s detected language, is decoded with timestamps so the next window can start where its last closed segment ended (no word is split by a boundary), and is decoded again when its text is a repetition loop (gzip compression ratio above 2.4): first without the conditioning text, then at rising temperatures — after which the transcript so far stops conditioning later windows. Leavetemperatureat 0 to get that fallback; a pinned temperature is used as is.long_form="parallel"inextra_bodysubmits fixed windows all at once, each cut at the quietest moment before its boundary. Segment timestamps are offset to the whole file.Realtime transcription — every
chunk_seconds(default 2 s) of new audio the session re-transcribes everything heard so far as one engine request whose assistant turn is prefilled with the previous hypothesis minus its lastunfixed_tokenstokens (the Qwen3-ASR SDK’s streaming algorithm), so only the tail is ever revised. Tunechunk_seconds/unfixed_chunks/unfixed_tokensundersession.mstarintranscription_session.update.Kokoro —
voiceis one of the 54 bundled voices (defaultaf_heart) or a blend such asaf_bella+af_skyoraf_bella(2)+af_sky(1);speedscales the speaking rate (0.25-4.0).lang_codeandphonemesgo throughextra_body.temperature/top_pare ignored: Kokoro does not sample.Orpheus — set the speaker with
voice— one oftara(default),zoe,zac,jess,leo,mia,julia,leah(theavailable_voiceslist in the Orpheus config).