Model server
Host open-weight models on your own hardware behind the Ollama and OpenAI APIs: pulls, residency with a fit check, admission queue, metrics; CPU and CUDA.
The OpenSmartRoute model server (osr-models) hosts open-weight models on your own hardware - a laptop, a VM,
a container, a GPU node - behind the APIs clients already speak. It is what the platform's self-hosted targets
run on, and it works on its own for anyone who wants a model server with a fit check, an admission queue and
metrics instead of a cold-load lottery.
| APIs | Ollama (/api/generate, /api/chat, /api/embed, /api/tags, /api/show, /api/ps, /api/pull, /api/create, /api/copy, /api/delete, /api/blobs, /api/version), OpenAI (/v1/models, /v1/chat/completions, /v1/completions, /v1/embeddings, /v1/responses, /v1/rerank), Anthropic Messages (/v1/messages), control (/models, /models/load, /models/unload), operations (/healthz, /readyz, /metrics) |
| Engine | llama.cpp llama-server, one process per loaded model, on CPU or NVIDIA CUDA; GGUF weights |
| Sources | ollama.com (qwen3.5:4b), Hugging Face GGUF repositories (hf.co/Qwen/Qwen2.5-0.5B-Instruct-GGUF:Q4_K_M, or a file inside a repository), local .gguf files |
| Store | content-addressed blobs and manifests in Ollama's on-disk layout: a store Ollama filled is served as-is, so switching costs no download |
| Residency | a model is loaded only when its footprint (weights + KV cache x context x slots + overhead) fits the memory budget after evicting idle models; a busy model is never evicted; keep_alive pins, expires or unloads |
| Admission | parallel slots per model, a bounded queue with a deadline, 503 + Retry-After past it (the platform re-routes to a cloud tier on that answer instead of waiting) |
| Metrics | Prometheus: requests, latency, tokens (prompt / cached / completion), slots, queue, resident bytes, loads, evictions, refusals - plus the engine's own series per model |
Run it
Docker (CPU)
docker run -d --name models -p 11434:11434 -v osr-models:/root/.ollama \
-e OSR_OLLAMA_MODELS="qwen3.5:4b qwen3-embedding:4b" -e OSR_OLLAMA_PRELOAD=1 \
opensmartroute/models
curl http://localhost:11434/api/chat -d '{"model":"qwen3.5:4b","messages":[{"role":"user","content":"Hello"}],"stream":false}'Docker (NVIDIA GPU) - the cuda image puts every layer on the GPU (OSR_MODELS_GPU_LAYERS to split):
docker run -d --gpus all -p 11434:11434 -v osr-models:/root/.ollama opensmartroute/models:cudaThe command line - the same binary inside the container manages the store and talks to the server:
docker exec models osr-models pull hf.co/Qwen/Qwen2.5-0.5B-Instruct-GGUF:Q4_K_M
docker exec models osr-models run hf.co/Qwen/Qwen2.5-0.5B-Instruct-GGUF:Q4_K_M "Why is the sky blue?"
docker exec models osr-models psOutside a container the package runs next to any llama-server binary from a llama.cpp release (point
OSR_MODELS_LLAMA_SERVER at it or put it on the PATH) and osr-models serve starts the server.
The server listens on Ollama's port (11434) and reads Ollama's variables, so an Ollama client, probe or
start-up script works unchanged; the container even answers to ollama list, ollama pull, ollama run.
Models
| Command | What it does |
|---|---|
osr-models pull qwen3.5:4b | from the ollama.com library (any registry speaking its protocol: host/namespace/name:tag) |
osr-models pull hf.co/<org>/<repo>:<quant> | the GGUF of that quantisation from a Hugging Face repository (Q4_K_M when no quantisation is named); a multimodal projector (mmproj-*.gguf) in the same folder comes along; HF_TOKEN for gated repositories |
osr-models pull hf.co/<org>/<repo>/<path>.gguf | one file of a repository |
osr-models pull ./model.gguf --name my-model | import a local file |
osr-models pull qwen3.5:4b=hf.co/unsloth/Qwen3.5-4B-GGUF:Q4_K_M | pull from one source, store under another name: every API, show and the platform's mapping see qwen3.5:4b while the weights come from the Hugging Face repository. Use it where the ollama.com blob is Ollama's own conversion that a standard llama.cpp cannot load (gpt-oss:20b, glm-4.7-flash, glm-ocr, translategemma:4b, qwen3.5:4b). POST /api/pull takes the same form in model, or the source in a source key |
osr-models list / ps / show MODEL / rm MODEL / stop MODEL | the store, what is in memory, a model's details and metadata, delete, unload |
POST /api/create | a new model from an existing one plus a system prompt, parameters (temperature, stop, num_ctx, ...), a template or adapters - the same body Ollama takes |
Pulls resume after an interruption, verify the digest, and are shared between replicas that mount the same
store through a lock in it. The deployment decides which sources are allowed (OSR_MODELS_SOURCES, default
ollama.com and Hugging Face).
A model's card is what POST /api/show returns: details (family, parameter count, quantisation),
model_info (the GGUF metadata - layer count, heads, trained context), capabilities (completion, tools,
thinking, vision, embedding, read off the weights), system, parameters, template, license. The system
prompt and parameters of a card apply to every request that does not set its own.
Requests
Everything the Ollama and OpenAI dialects define for chat works on both paths: streaming, tools and tool calls,
images (multimodal models), format: "json" or a JSON schema (response_format on /v1), think levels
(reasoning_effort on /v1 - none switches a thinking model's reasoning off so it answers inside a small
max_tokens), keep_alive, the Ollama options (temperature, top_p, top_k, min_p, num_predict,
seed, stop, penalties). Answers carry Ollama's statistics (prompt_eval_count, prompt_eval_cached_count,
eval_count, durations in nanoseconds, load_duration on the request that loaded the model) and the OpenAI
usage with cached_tokens. The prompt cache is on: a repeated system prompt or conversation prefix is not
evaluated again.
An empty request loads a model ({"model": "...", "messages": []}), keep_alive: 0 with it unloads;
GET /api/ps lists what is resident with its slots, queue and expires_at (0001-01-01 = pinned).
Settings
Every setting is an environment variable; the OSR_MODELS_* name wins over the Ollama name it mirrors.
| Setting | Ollama name | Default | Meaning |
|---|---|---|---|
OSR_MODELS_HOST | OLLAMA_HOST | 127.0.0.1:11434 | listen address |
OSR_MODELS_STORE | OLLAMA_MODELS | ~/.ollama/models if present, else ~/.osr/models | the store directory (/root/.ollama/models in the image) |
OSR_MODELS_PRELOAD | OSR_OLLAMA_MODELS | - | models pulled at start (name or name=source; a name=source entry whose stored weights came from anywhere else - an earlier ollama.com pull of the same tag - is pulled again from the source); with OSR_OLLAMA_PRELOAD=1 loaded and pinned too |
OSR_MODELS_CONTEXT | OLLAMA_CONTEXT_LENGTH | 8192 | context per slot (clamped to the model's trained window and its num_ctx) |
OSR_MODELS_PARALLEL | OLLAMA_NUM_PARALLEL | 1 | generations one model decodes at once (embedding models always 1) |
OSR_MODELS_KV_CACHE | OLLAMA_KV_CACHE_TYPE | f16 | KV cache precision (q8_0 halves it; q4_0 quarters it) |
OSR_MODELS_MAX_LOADED | OLLAMA_MAX_LOADED_MODELS | 0 = as many as fit | cap on resident models |
OSR_MODELS_KEEP_ALIVE | OLLAMA_KEEP_ALIVE | 5m | idle time before a model is unloaded; -1 = never |
OSR_MODELS_QUEUE_MAX | OLLAMA_MAX_QUEUE | 4 x parallel | requests that may wait for a slot before 503 |
OSR_MODELS_QUEUE_TIMEOUT | - | 20s | longest wait for a slot |
OSR_MODELS_LOAD_TIMEOUT | OLLAMA_LOAD_TIMEOUT | 20m | longest load (a network share is slow) |
OSR_MODELS_REQUEST_TIMEOUT | - | 15m | longest silence from an engine during one request (a thinking model on a CPU can be quiet for minutes) |
OSR_MODELS_DEVICE | - | auto | cpu, cuda, or detect |
OSR_MODELS_GPU_LAYERS | - | all | layers offloaded to the GPU |
OSR_MODELS_THREADS | - | engine default | CPU threads per engine |
OSR_MODELS_FLASH_ATTENTION | OLLAMA_FLASH_ATTENTION | on | flash attention (forced on for a quantised KV cache) |
OSR_MODELS_MEMORY | - | detected (cgroup limit or physical memory) | the memory budget before headroom, e.g. 28Gi |
OSR_MODELS_HEADROOM | - | 0.12 | share of memory kept for the OS and page cache |
OSR_MODELS_SOURCES | - | registry.ollama.ai huggingface.co hf.co | registries pulls may use |
OSR_MODELS_HF_TOKEN | HF_TOKEN | - | Hugging Face token for gated repositories |
OSR_MODELS_TOKEN | - | - | bearer token required off the loopback (/healthz, /readyz, /api/version stay open) |
OSR_MODELS_SLEEP_IDLE | - | off | seconds of idleness after which an engine releases its memory but keeps its process |
OSR_MODELS_LLAMA_SERVER | - | PATH | the engine binary |
Sizing is arithmetic the SDK does for you: osr capacity show <model> --memory 28Gi --cores 4 prints a
model's footprint, whether it fits and the expected tokens/s; osr capacity plan <models...> --memory 28Gi
packs a set onto hosts; osr capacity ps --url http://<server>:11434 lists what the server holds right now.
With the platform
The platform treats a model server as an ollama provider: its base URL (http://<server>:11434/v1) in
OSR_PLATFORM_PROVIDERS or the operator console, targets mapped to model names, concurrency equal to the
slots the server offers. The provider check reads /api/ps for residency, the keep-warm pass loads mapped models
that are not resident, and the calibrator turns the server's measured latency into what the router ranks on.
The Docker Compose stacks run the model server as the llm profile; the Azure template's self-hosted model
apps run it when OSR_OLLAMA_IMAGE names the model server image (the Ollama image stays the default) -
same volume, same start-up, same probes.
Operations
GET /healthz- engine version, device, instances, memory budget and use, store usage, preload progress and errors;GET /readyz-200once the preload list is pulled (and loaded when asked),503before.GET /metrics-osr_models_requests_total,osr_models_request_seconds,osr_models_tokens_total,osr_models_slots_busy,osr_models_queue,osr_models_resident_bytes,osr_models_loads_total,osr_models_evictions_total,osr_models_refused_total,osr_models_load_seconds, and the engine'sllamacpp:*series labelled by model.GET /models- every model in the store with its status (unloaded,loading,loaded), footprint and the engine arguments;GET /models/<id>/logs- the engine's last log lines when a load fails.- A load that does not fit answers
507with the budget and what to change (shorter context,q8_0KV cache, a smaller quantisation). A saturated model answers503withRetry-After. - Prompts and answers are never written to disk; the engine processes bind to the loopback only.
Something wrong or out of date on this page? Tell us - the page is named for you, so a one-line note is enough.