7.20. dasllama-server — an OpenAI-compatible server over dasLLAMA
dasllama-server (directory: utils/dasllama-server/) is a drop-in
OpenAI-compatible HTTP server for dasLLAMA
inference - on the CPU, or with the Metal and Vulkan GPU tiers - written
entirely in daslang over the public dasllama facade plus the
dasHV HTTP layer. Point any OpenAI client
(opencode, Open WebUI, the llm CLI, the openai Python SDK, …) at
http://127.0.0.1:<port>/v1.
It reaches only public facade verbs — load_model,
create_chat_renderer / add_user / render_assistant / render_turn,
eval_batch through dasllama_scheduler, transcribe, embed —
and that is the point: the server is the acceptance test for the dasLLAMA API.
If a full OpenAI surface builds with no reach into engine internals, the
facade is complete.
7.20.1. Get it
The server ships as the dasllama package, refreshed with every daslang
release:
Platform |
Install |
|---|---|
macOS, Apple silicon |
|
Windows x64 |
|
Debian, Ubuntu |
|
Fedora |
|
Linux (Homebrew) |
|
any, with Python |
|
Or unpack an archive from the rolling dasllama-server release,
https://github.com/GaijinEntertainment/daScript/releases/tag/dasllama-server:
dasllama-darwin-arm64.zip (macOS, Apple silicon),
dasllama-windows-x64.zip, dasllama-linux-x86_64.tar.gz and
dasllama-linux-arm64.tar.gz.
Every form carries the same four programs. A package manager puts them on PATH
as dasllama-server, dasllama-cli, dasllama-bench and
dasllama-watchdog. Inside an archive they are dasllama-server.exe,
dasllama-cli.exe, dasllama-bench.exe and watchdog (watchdog.exe
on Windows) on Linux and Windows alike; the macOS .app carries them without
the suffix in Contents/MacOS.
The Linux builds need glibc 2.35 or newer (Debian 12, Ubuntu 22.04 and later),
the system OpenSSL 3 (libssl3 on Debian and Ubuntu, openssl-libs on
Fedora) and curl for the catalog downloads - the .deb and .rpm pull
them in. The macOS and Windows builds carry their own OpenSSL.
Start the supervisor - dasllama-watchdog (watchdog in an archive), or
on macOS the dasllama-server.app itself, whose launcher is the watchdog.
brew services start dasllama starts it at login. The watchdog keeps the
server up, puts the dasllama mark in the notification area, and a click on it
opens the control page at http://127.0.0.1:8080/. With no model configured
the server starts in setup mode: pick a model from the catalog, it downloads
into ~/.dasllama/models, and serve this model restarts into it.
Everything the programs write lives under ~/.dasllama
(%USERPROFILE%\.dasllama on Windows), never beside them. So an installed
copy stays read-only, and an upgrade keeps it all:
Path |
What |
|---|---|
|
the catalog downloads, and the |
|
the config the control page saves |
|
the box’s runtime section: the Metal kernel choices raced at the first start, the knobs |
|
|
--config names a config outright. Without it, the programs look for
dasllama-server.toml in the working directory, then in ~/.dasllama,
then beside the program. A config or sidecar an earlier download kept beside
the exe still reads - the sidecar is copied to tune/ at the first start. A
JIT run from the SDK keeps its logs in the SDK’s logs/.
The exe is a fat build (daspkg release --fat): plain code for the
platform’s baseline CPU class - x86-avx2 on x86, arm-neon on arm64 -
with one clone of every [tune] kernel per class the engine ships a profile
for (x86-vnni512, x86-amx, arm-i8mm), picked from cpuid at start.
On a Mac the Metal kernel choices are raced once at the first start and kept
in ~/.dasllama/tune/. That is the good default, and it is as far as a solid
executable goes: a fat build cannot tune - its kernels are baked, it carries no
tuner, and --tune, the sidecar exchange and a re-tune have nothing to act
on (the control page’s exchange levers refuse). To tune every kernel for this
box, install daslang with dasLLAMA (the SDK) and run the server through the
JIT - the command under Run from the source tree - or daspkg release the
package on that box: either mints the box’s own sidecar, every kernel raced on
your own hardware instead of picked from a class profile.
The builds are not code-signed. macOS asks once under System Settings >
Privacy & Security before the cask’s first start. An app unpacked from a
downloaded archive is quarantined, and started from the download folder it runs
from a read-only copy - so first drag dasllama-server.app out of Downloads
with the Finder, then either allow it under System Settings > Privacy &
Security after the first refused start, or clear the flag once with
xattr -dr com.apple.quarantine dasllama-server.app. Windows SmartScreen
shows an unknown publisher: More info, Run anyway.
7.20.2. Measure your box
The control page’s benchmark button runs the llama-bench rows on the served
model right in the server - pp512 and tg128, three timed reps each after an
untimed warmup, and on a slot that serves two streams or more tg128@N, N
streams (up to four) through the scheduler, the summed rate - and shows tokens
per second beside the hardware line. Every route that could contend holds for
the run (the text, audio and speech routes answer 503, the model-switching,
bake and download routes 409). A run is seconds on a small model. The same
rows from a shell, and the llama.cpp comparison the page cannot run itself,
come from dasllama-bench (on PATH from a package; in an archive
dasllama-bench.exe beside the server on Linux and Windows,
dasllama-server.app/Contents/MacOS/dasllama-bench on a Mac):
dasllama-bench -m ~/.dasllama/models/<model>.gguf # pp512/tg128, five reps
dasllama-bench -m <model.gguf> --ref /path/to/llama-bench # and llama.cpp on the same GGUF
The two run the same rows, not the same numbers: the page measures the served
slot as it is - its device, its KV codec - while dasllama-bench runs f16
KV on the CPU unless -ngl says otherwise, five reps to the page’s three.
The page’s result names what produced it: the device that served the rows,
the KV codec, the exec tier (exe-native in a bundle, jit from the SDK),
the tune state (fat in a bundle; tuned or untuned under the JIT),
and the llama-bench line that matches it.
7.20.3. Run from the source tree
Run under -jit — the interpreter is refused, it is far too slow for inference:
bin/daslang -jit utils/dasllama-server/main.das -- --model <model.gguf> [--port 8080] [--quant q8] \
[--asr <asr.bin>] [--asr-workers 2] [--mmproj <mmproj.gguf>] [--image-mmproj <mmproj.gguf>] \
[--audio-mmproj <mmproj.gguf>] \
[--ctx 4096] [--streams 4] [--chunk 512] [--chunk-idle 2048] [--page-rows 64] [--prefix N] \
[--prefix-states 256] [--prefix-state-mb 0] [--tune]
Flag |
Short |
Default |
Meaning |
|---|---|---|---|
|
|
auto |
TOML config file; keys mirror the long flag names, explicit CLI flags override. Without the flag, a |
|
|
(required) |
GGUF model to serve (here or in |
|
|
|
Listen port |
|
|
Interface to listen on. The default serves this machine only; |
|
|
|
|
Weight serving mode: |
|
|
KV-cache codec: |
|
|
|
GPU backend: |
|
|
|
Metal serving mode when |
|
|
auto |
vulkan: resident MoE expert-stack layers, offloaded from the end (config key |
|
|
auto |
vulkan: streamed prefill MoE layers below the resident set (config key |
|
|
on |
vulkan: deltanet (recurrent) layers through the device chain |
|
|
on |
vulkan: full-attention layers through the device chain |
|
|
off |
vulkan: dense attention-side planes resident |
|
|
device |
vulkan: resident-weight VRAM cap override in MB (default: query the device) |
|
|
|
— |
ASR model (whisper / parakeet / qwen3-asr) — enables the |
|
|
Long-lived ASR request threads; each owns a model and a reusable session. |
|
|
— |
mmproj GGUF for the Qwen3-ASR route (paired with |
|
|
— |
TTS model GGUF (kitten-nano, kitten-mini, kokoro-82m with the front-end packs |
|
|
|
Weight lane the speech worker pins around its load: |
|
|
— |
Directory of voice clips (wav / flac / mp3 / ogg, a few seconds of one speaker each, 60 s at most) a cloning speech model adds to its voices at boot (with several TTS models, the first one’s), each under its file’s stem; a clip that does not decode or runs past 60 s is logged and skipped, the key on a model that cannot clone is logged and ignored |
|
|
— |
Vision mmproj (gemma4uv, gemma4v or gemma3v, sniffed) for the default model — the chat route then accepts |
|
|
— |
Audio mmproj for the default model — the chat route then accepts |
|
|
model |
Context length in tokens, served whole: the whole-model GPU driver holds it for every stream or declines to the per-op rails, and the load log names the room. Default: the model’s trained |
|
|
|
Default reply token budget when a request omits |
|
|
|
|
Max concurrent generation streams |
|
|
|
Worker-lane cap for the matmul dispatch ( |
|
|
|
|
|
|
Worker CPU affinity: |
|
|
backend |
Prefill quantum in tokens while a stream is decoding: decode stalls at most this many per tick. Default: 512 where a GPU prefills (a window’s cost is mostly fixed there, so a small chunk runs a prompt at a fraction of the window’s rate), 64 on the CPU (a chunk costs its token count, so a short one keeps the stall short at 13 - 29% of the prompt’s rate) |
|
|
backend |
Prefill quantum in tokens while no stream is decoding (never under |
|
|
|
KV page size in positions for paged serving |
|
|
auto |
Prefix-cache retention cap in pages (auto: one full context per stream; |
|
|
|
Recurrent models (the Qwen3.5/3.6/3.8 hybrids) cache whole checkpoints instead of pages: the most a slot keeps ( |
|
|
auto |
The checkpoints’ byte budget in MB, snapshots and held pages (auto: a quarter of
the box’s RAM - in a container, of its memory limit where that is lower; config
key |
|
|
— |
Flat preallocated KV sessions — disables paged serving and the prefix cache |
|
|
off |
MTP/NextN self-speculative decode. Unset, a slot leaves it off, except one that runs one stream ( |
|
|
file |
RoPE scaling override for the load: |
|
|
file |
The scaling factor |
|
|
file |
YaRN: the original training context the factor extends; unset reads the file’s |
|
|
— |
Path to a llama.cpp |
|
|
|
Where the model catalog downloads land ( |
|
|
— |
Re-tune this box’s dasLLAMA kernels, then relaunch (see Per-box tuning; a fat build carries no tuner and ignores it) |
|
|
|
— |
Show help and exit |
A config file replaces long command lines; keys are the long flag names with
underscores. A dasllama-server.toml in the cwd, in ~/.dasllama, or next
to the program loads automatically, the first one found; --config other.toml
picks a different one:
model = "D:/models/SmolLM2-135M-Instruct-Q8_0.gguf"
port = 8080
host = "127.0.0.1" # the interface: this machine only; "0.0.0.0" opens it to the network
quant = "q8"
kv_dtype = "tq4" # rotated 4-bit KV - half the q8_0 cache bytes
ctx = 4096
max_tokens = 4096 # default reply budget for clients that omit max_tokens
streams = 4
threads = 16 # matmul dispatch lane cap; -1 = all cores
team_dispatch = "hybrid"
asr_workers = 2 # two independent transcription requests; each worker owns an ASR model
Several speech models serve side by side, each on worker threads of its own, so
a request to one never waits behind another. asr and tts are then
rosters instead of paths — a table a model, the first the default (the model a
request with no model field gets):
[[asr]]
path = "models/parakeet-tdt-0.6b-v3.bin"
[[asr]]
id = "qwen-asr" # the id requests name; default: the file's stem
path = "models/Qwen3-ASR-1.7B-Q8_0.gguf"
mmproj = "models/mmproj-Qwen3-ASR-1.7B-bf16.gguf"
workers = 2
[[tts]]
path = "models/kokoro-82m.gguf"
[[tts]]
path = "models/pocket-tts-en-q8.gguf"
voices_dir = "voices"
lane = "q8"
With a roster, the flat mmproj / asr_workers / tts_lane /
tts_voices_dir config keys are not read — each table carries its own —
while the same flag on the command line overrides the first table’s value. Two models
may not share an id: the second is logged and left out.
Config precedence is defaults < config TOML < explicit CLI flags - unless
the TOML carries authoritative = true (what the control page saves), which
flips the top: defaults < CLI < authoritative TOML.
Several models serve live from one process through a [[models]] roster
instead of the flat model key. Requests route on their "model" field
(absent means the default entry). Execution is serialized - one scheduler steps
at a time - switches are fast, and every cache level survives a switch: host
weights stay mmap’d, each slot keeps its own KV pool and prefix cache, and one
model’s GPU state lives in VRAM at a time (the tier drops and re-arms on
switch; backend = "cpu" slots never evict the GPU owner). Blank keys
inherit the flat defaults; backend is auto | cpu | gpu, and
per-entry ctx, quant, kv_dtype, streams, chunk, chunk_idle,
page_rows, prefix, mtp, rope_scaling, rope_scale, yarn_orig_ctx, image_mmproj and audio_mmproj override per model:
[[models]]
name = "qwen" # route id (default: the file's basename)
path = "D:/models/Qwen3.6-35B-A3B-Q8_0.gguf"
default = true
[[models]]
name = "smol"
path = "D:/models/SmolLM2-135M-Instruct-Q8_0.gguf"
backend = "cpu" # never touches the device - alternating with the GPU slot costs nothing
An explicit --model wins over the roster: the entry with that path becomes
the default, and a path the roster does not list joins it as the default. A
missing file, or a route name another entry already takes, is logged, and the
roster boots unchanged.
Chat and completion requests batch continuously (dasllama/dasllama_scheduler.das):
up to --streams generations run concurrently through one eval_batch
decode step per tick, with long prompts prefilled in --chunk-token slices,
so a new arrival never stalls the running streams for more than one chunk
(with no stream running, the slices are --chunk-idle tokens).
Requests beyond --streams queue (up to 32; then 503). KV is paged by
default - cache memory tracks each stream’s actual context, and finished
streams donate their pages to a prefix cache, so a repeated prompt prefix (a
shared system prompt, the next turn of the same conversation) attaches instead
of re-prefilling. Clients whose connection drops mid-generation are evicted
within a tick. Audio uploads queue to long-lived ASR workers and do not block
chat generation. OpenAI is stateless — the client resends the full transcript
each turn.
7.20.4. Setup mode and the model catalog
A start with no LLM model at all - no --model, no config, or every
configured path missing - and no servable --tts or --asr boots into setup mode: the
port opens, the control page serves, every inference route answers with a
clean error, and the page leads with the model catalog - a curated, sha-pinned
list of current models (model_catalog.das, a view over the module’s one
model set; commit-pinned HF URLs, canonical sha256, one download at a time,
curl -C - resume). A finished download is verified against its pinned sha
before it is renamed into models_dir with the .sha sidecar beside it. In
setup mode a downloaded entry offers serve this model - the page writes the
config and restarts (exit 4, the watchdog contract) into serving it. The same
catalog stays available on a serving server for pulling more models.
Catalog entries carry their towers: a vision-capable row offers its pinned
mmproj (download, enable vision, restart), a dictation strip under the table
offers the ASR tower (parakeet v3), and a speech strip offers the text-to-speech
set - the two front-end packs, then one model, or a Pocket file that stands
alone. Setup-mode serve this model wires any tower already on disk. Each row
wears a fit badge (fits gpu / fits / tight / too big) from the box facts the
/catalog document carries; the advertised working set is a hint, not a load
gate.
--tts or --asr alone is a serving start, not a setup start: a speech-only
server has no LLM slot, so /v1/stats answers the slotless shape with setup
false and its speech counters live. An ASR model whose decoder is an LLM - a
two-file model, --asr with --mmproj - decodes where the server’s LLMs do:
its worker takes the boot’s Metal mode, and /v1/stats names the outcome under
asr.models[] as decoder (metal or cpu). A one-file model (whisper,
parakeet) decodes on the CPU.
A --tts the server cannot serve degrades, it does not die: a missing file
or a failed load is logged, the speech route is dropped, and the boot serves
whatever is left - the LLM slots if any loaded, else setup mode.
7.20.5. Per-box tuning
This section is the JIT run’s - the SDK path. The standalone download is a fat
build: one baked clone of every kernel per CPU class, picked at start, no
tuner, no sidecar of its own (only the runtime section its first start writes
into ~/.dasllama/tune/); nothing below applies to it, and --tune is
ignored there.
The server declares [tune_policy(missing = "restart")]. The first start on
an untuned box adopts the shipped profile for its CPU class, and what the
profile does not answer runs the dasLLAMA kernel tuner. A tune writes the
sidecar and exits with code 3, before the port opens; the watchdog relaunches
the server with the winners (without a watchdog, start it again). Thereafter it
serves directly and logs the tune status at startup. A start with no model
configured skips the tune, so the setup page opens at once. --tune forces a
re-tune. DAS_TUNE_POLICY=error skips per-start tuning while developing (it
prints the tuner command instead of running it).
The winners live in a sidecar beside the script the JIT runs:
utils/dasllama-server/main.tune.json for the server
(DAS_TUNE_MANIFEST names another path). dasllama-cli
declares [tune_policy(missing = "auto")] - it tunes, then relaunches
itself - and keeps its own sidecar beside cli.das. See
Kernel tuning for the framework.
The sidecar exchange - dasllama.io’s tune-sidecar service - is closed by
default: the three config-only keys exchange_accept (verified | any
| off, default off), exchange_submit (ask | always |
never, default never) and exchange_url are the only way in. A build
without dasLLVM answers 404 on every /exchange* route, and a fat release
refuses the levers.
7.20.6. The gpu key
gpu (auto | off | metal | metal-required | vulkan) is
the backend selector, and unset behaves as auto: the boot probes the box and
serves on the best detected backend - the Metal rails where the box has them,
else the Vulkan tier when a device answers, else the CPU - logging one
gpu backend auto-detected: line. gpu = off is the explicit opt-out, and
DASLLAMA_GPU=0 in the environment keeps its meaning. A model the armed tier
cannot serve falls back to the CPU with the reason on the control page.
gpu = vulkan asks for the whole-model resident driver first: a model that
fits the card serves entirely from VRAM, and so do its streams - the driver
holds a K/V region per stream, the prompt prefills on the device in 512-token
windows, the streams decode in one batched step, and a conversation’s next turn
picks up its earlier rows from the region they were left in. /v1/stats
counts every call the device handed back to the CPU (gpu_cpu_passes); a
slot served this way keeps it empty. The control page’s model card says which
of these a slot got (served) and why it got less when it did
(served_note). A recurrent (deltanet) model serves the same way, its
recurrent state kept in the stream’s region beside its rows; a recurrent model
with hyper-connections (Qwen3.8-Flash-Next) differs in two ways: a prompt
prefills in one piece, and a next turn re-prefills its whole transcript. A slot with a vision or audio tower keeps host-cached
sessions, and so does a self-speculative slot where any seat of the round (the
draft head, its verify, a rejected round’s rollback) stays on the host. Beneath the driver the per-op rails arm as the
fallback for a model that does not fit: expert stacks sized automatically
(resident layers fill the VRAM budget, the rest stream) plus the deltanet
rail (each deltanet plane in its own format), the attention rail, the dense
layers’ attention projections and the shared expert, each over its own planes;
a dense layer’s FFN rides the per-op FFN rail (DASLLAMA_GPU_DENSE_FFN=0 keeps it on the CPU). The config keys gpu_layers / gpu_stream are
0 = auto; gpu_dn / gpu_attn / gpu_dense / gpu_vram_mb
override the rest, and the DASLLAMA_GPU_* environment variables override
everything.
7.20.7. Endpoints
Method |
Path |
Notes |
|---|---|---|
|
|
The control page ( |
|
|
Lists every served slot, every TTS model under its id, and the ASR models: the default one as |
|
|
|
|
|
|
|
|
|
|
|
Chat; |
|
|
Raw completion; |
|
|
L2-normalized sentence embeddings, pooled the way the model’s file asks ( |
|
|
Speech to text (multipart upload; needs |
|
|
Speech to English text (needs |
|
|
Text to speech (needs |
|
|
|
|
|
Scheduler counters ( |
|
|
Per-stream states and text tails, the prefix-cache chains, recent ASR jobs |
|
|
Per-slot prepared-image ( |
|
|
Silero speech spans over an uploaded clip — the control page’s waveform overlay (120 s at most) |
|
|
The curated model list with local presence, the |
|
|
|
|
|
Read the bench state, mode, log and result / start the quiesced benchmark: in process by default (pp512 and tg128 on the served model, then tg128@N on a slot serving two streams or more), or the llama.cpp A/B child when |
|
|
Read the bake state and log / bake the slot’s prepared |
|
|
The effective config with per-key source ( |
|
|
The tune-sidecar exchange surface: policy, the consent state, the current sidecar’s identity and share state |
|
|
Live lookup of this box against the exchange (a network call — seconds) |
|
|
|
|
|
Privacy-strip and submit this box’s own tune to the exchange |
|
|
Arm a local re-tune and restart — the next boot races this box |
|
|
|
|
|
Schedule a validated collection at the next lifecycle safe point; concurrent requests coalesce |
|
|
Loopback-only: drop every slot’s prompt cache (donated pages and checkpoints) while the model stays loaded, so the next request prefills cold; 409 while streams are live |
|
|
Drain, then exit 4 — the watchdog relaunches with the saved config |
|
|
Stop admitting new work, drain, then exit |
Unknown endpoints answer 404 through a catch-all that logs the method, the path and the body head; known routes warn per ignored field.
7.20.7.1. Chat
curl http://127.0.0.1:8080/v1/chat/completions -H 'Content-Type: application/json' -d '{
"messages": [{"role": "user", "content": "Say hello in one word."}],
"max_tokens": 16, "stream": false, "truncation": "auto"
}'
7.20.7.2. Sampling parameters
Both completion routes accept the OpenAI sampling fields plus the llama.cpp extensions; an absent field keeps the greedy default.
Field |
Default |
Meaning |
|---|---|---|
|
|
|
|
|
keep only the k highest logits |
|
|
nucleus: keep the smallest probability mass |
|
|
drop tokens with probability |
|
|
multiplicative repetition penalty over the recent window, once per unique token |
|
|
flat logit subtraction for every distinct token in the recent window |
|
|
per-occurrence logit subtraction |
|
|
non-zero overrides the stream’s RNG seed; identical no-seed requests already reproduce |
The cutoffs shape the sampled distribution, so they act only with
temperature > 0; the penalties apply in greedy mode too.
7.20.7.3. Thinking and reasoning_content
enable_thinking is tri-state. Absent, the model template’s own default is
in force (Qwen3/3.5/3.6, GLM, gemma-4 and gpt-oss templates default on; a bare
Qwen Instruct branch defaults off); a present bool - top-level or the
llama.cpp spelling "chat_template_kwargs": {"enable_thinking": ...} -
overrides it. false on a <think> family appends the template’s empty
think block so the model answers directly; false on gemma-4 prefills the
closed empty thought channel. A thinking reply’s reasoning span comes back as
reasoning_content with content clean of the family’s markers: on the
message for buffered requests, and as delta.reasoning_content chunks,
streamed before the delta.content chunks, for stream: true. The field is
absent when the model did not think.
7.20.7.4. Context truncation
An over-context rendered prompt returns 400. truncation: "auto" keeps the
system messages and the tools and drops the oldest complete user-led turns
until the prompt plus the requested max_tokens budget fits; when the
system, tools and latest turn cannot fit, the request still returns 400.
finish_reason: "length" means generation consumed its output budget.
7.20.7.5. Tool / function calling
/v1/chat/completions speaks the OpenAI function-calling protocol: pass
tools (and tool_choice: "none" disables, "required" acts as
"auto", the forced-function object form is not honored), get back finish_reason: "tool_calls" with
message.tool_calls, send the results as role: "tool" messages, repeat.
Assistant tool_calls turns and role: "tool" results replay through the
chat template on each stateless resend, so agent loops (opencode, pi, …) work
end to end. A thinking model that calls tools returns reasoning_content and
tool_calls in one response.
The wire format follows the model family: hermes (Qwen2.5 / Qwen3 family -
a <tools> system block, <tool_call> JSON), harmony (gpt-oss -
developer-turn TypeScript namespace defs, commentary-channel recipient calls),
gemma4 (gemma-4 - the declaration / call / response DSL), mistral
(v0.3 and the tekken templates - Mistral-Small 3.x, Magistral, Devstral:
[AVAILABLE_TOOLS] defs, a [TOOL_CALLS] array) and
llama_json (llama-3.x - the whole reply is one {"name","parameters"}
object, results on the ipython role). A model whose chat template declares
no tool format gets a 400. Streaming with tools puts the reply’s text on the wire
as it comes and holds back only the family’s call marker; the parsed calls arrive
as one delta.tool_calls chunk at finish; hermes, xml_function and gemma4 hold
at their call marker, llama_json at the call object’s opening brace, mistral at the
call array’s bracket. A family whose calls ride channels rather than a marker
(harmony) is framed whole at the finish.
7.20.7.6. Images
A slot started with --image-mmproj accepts the OpenAI content-parts image
form. The URL must be a data: URI - the server never fetches a remote URL:
curl http://127.0.0.1:8080/v1/chat/completions -H 'Content-Type: application/json' -d '{
"messages": [{"role": "user", "content": [
{"type": "image_url", "image_url": {"url": "data:image/jpeg;base64,'"$(base64 < cats.jpg | tr -d '\n')"'"}},
{"type": "text", "text": "What animals are in this picture?"}
]}], "max_tokens": 64
}'
The image is decoded and encoded on a dedicated vision worker thread, and its
soft-token rows prefill between the two token spans of the rendered turn. One
image per request, on the final user message: two images is a 400, an image
anywhere else is dropped with a warning. A slot with no vision arm answers 400.
An image stream neither reads nor writes the prefix cache, and
usage.prompt_tokens counts the soft-token rows as positions. The payload
caps at 32 MB of file and 67 MP decoded.
7.20.7.7. Embeddings
input is a string or an array of strings. Each vector is model.config.dim
floats from the decoder’s last-layer hidden rows (post-final-norm), L2-normalized,
pooled the way the model’s file asks: its pooling_type key names last-token
pooling for an embedding model such as Qwen3-Embedding (the row at the appended end
token, the way it was trained to be read) and is absent on a chat model, which is
read by the mean over positions. pooling ("mean" | "cls" | "last")
overrides the file; dimensions keeps the leading components and renormalizes,
the Matryoshka read the Qwen3-Embedding family supports. An instruction prefix on
the query side (“Instruct: …nQuery: …”) is the client’s text, as the API has no
field for it. A chat model used as an embedder gives RAG-grade vectors (good for
retrieval / similarity), not a substitute for a dedicated embedding model. See
dasLLAMA-09 — Embeddings for the facade side.
curl http://127.0.0.1:8080/v1/embeddings -H 'Content-Type: application/json' -d '{
"input": ["the quick brown fox", "a lazy dog"]
}'
# -> {"object":"list","data":[{"object":"embedding","embedding":[...],"index":0}, ...],
# "model":"...","usage":{"prompt_tokens":N,"total_tokens":N}}
7.20.7.8. Transcription (with --asr)
curl http://127.0.0.1:8080/v1/audio/transcriptions \
-F file=@audio.wav -F response_format=verbose_json
ASR work runs on its own worker threads beside the HTTP tick and the LLM scheduler. Requests beyond the worker count wait in a bounded queue (32 uploads; then 503).
7.20.8. Supervised deployment
The server runs under the shared watchdog in utils/watchdog/, in the
release bundle and in a JIT deployment alike. The watchdog needs no arguments:
in the bundle it finds the baked exe beside it (watchdog.json names
dasllama-server among the bundle’s programs), in a JIT deployment
main.das beside bin/Release/daslang.exe. watchdog.json also pins the
name, turns the tray on, names tray.ico as its mark, and points the state
directory at ~/.dasllama, so the watchdog’s log lands in
~/.dasllama/logs/dasllama-watchdog.log. From the source tree the watchdog
does not sit beside the script, so pass --cwd:
bin/Release/watchdog.exe --cwd utils/dasllama-server --jit-stack
The first JIT start on an untuned box writes the tune sidecar and exits with
code 3; the watchdog recognizes that bootstrap exit and relaunches. It writes
rotating JSON-line logs, polls /v1/models, and after a crash copies the
minidump with the matching JIT artifacts, tune manifest and log into
~/.dasllama/logs/crashes/, then restarts with bounded exponential backoff.
daspkg release --root utils/dasllama-server --out <dir> bakes the server
into a standalone bundle: the exe, the shared modules and runtime libraries it
needs, watchdog beside it, dasllama-cli, dasllama-bench,
watchdog.json, control.html and tray.ico. Plain release tunes
the kernels on the build box and ships that box’s sidecar; release --fat
x86-avx2 (arm-neon on arm64) is what the public download is built from.
The config and the tune sidecar live under ~/.dasllama, so an upgrade
unpacked over the old bundle keeps them; stop a running server first - Windows
locks the DLLs.
7.20.9. Testing
The tests in the tool directory are model-gated and JIT-only: they skip
cleanly when the GGUF is absent (set DASLLAMA_MODELS_DIR, one of the knobs
in the dasLLAMA knob reference):
bin/daslang -jit dastest/dastest.das -- --test utils/dasllama-server/test_openai_server.das
test_openai_server.das— endpoint conformance (/v1/models,/v1/embeddings, buffered chat, the tools-unsupported 400, the unknown-endpoint 404) over the real dashv HTTP client, plus model-freeparse_tool_callsunit tests; needstinyllama-1.1b-chat-v1.0.Q8_0.gguf.test_openai_server_stream.das— SSE chunk framing, the over-long-prompt 400, two concurrent clients batching on one server, mid-generation disconnect eviction, the prefix cache returning an identical completion for a repeated request; needsSmolLM2-135M-Instruct-Q8_0.gguf.test_openai_server_vision.das— the image route end to end on gemma-4-12B with its mmproj and the coco cats jpeg.test_openai_server_speech.das— the speech route end to end on a TTS-only boot; needskitten-nano.ggufand the front-end packs beside it.test_openai_server_audio.dasandtest_openai_server_think.das— the native-audio content parts and the thinking control.test_openai_server_mtp.das— the self-speculation default: a NextN-headed slot whose file has no catalogserve_mtprow decodes plain at one stream on Metal, at four and on the CPU, an explicitmtpwins either way, a head-less model with no drafter beside it serves plain; the Metal cells skip on a box with no Metal backend; needsQwen3.5-0.8B-MTP-Q8_0.ggufand the TinyLlama file.test_exchange_client.das,test_model_catalog.das,test_setup_mode.das— model-free: the sidecar exchange client against a fake exchange, the catalog-table invariants, a slotless setup-mode boot.test_slot_served.das,test_model_override.das,test_worker_dispatch.das,test_server_flags.das— model-free: everyserved/served_notearm, the--model-over-roster override, the media worker’s dispatch under the job queue, the flag-presence test behind the config surface’s provenance and the self-speculation default’s rule.tests/— the control page itself under Playwright (Node + chromium), against JSON/SSE fixtures captured from a real server;npm ci && npx playwright testin that directory.
server_bench.das (same directory) measures the serving latencies through
the scheduler seam: tok/s, TTFT and inter-token percentiles against batch size,
the decode stall per prefill chunk size, and warm against cold TTFT for the
prefix cache.
7.20.10. Not yet implemented
On the media path: more than one media clip per request, media on earlier
turns of a conversation, and remote image_url fetches.