Connect model clients¶
loom-serve can back Pi, Oh My Pi and Open WebUI through their existing
OpenAI-compatible configuration. No client plugin or Loom-specific HTTP route
is required. The server continues to publish one complete catalog containing
text, image, embedding, decision, speech and recognition deployments; each
client selects the operations it understands.
This workflow assumes that the server and its model packages are already
running. Serve source-defined models establishes that
baseline. The examples use scout for text, illustrator for image generation
and vectors for embeddings; replace those names with IDs from the live
catalog.
The speech workflow covers clients that consume raw PCM incrementally. The audio settings below connect completed-recording dictation and buffered playback to separate recognition and speech deployments.
Inspect the client-visible contract¶
Set the base URL to the address visible from the client process and inspect the catalog before configuring a client:
loom_server_url=http://127.0.0.1:8080
loom_base_url=$loom_server_url/v1
curl --fail --silent --show-error "$loom_base_url/models" |
jq '.data[] | {
id,
input_modalities,
output_modalities,
context_length,
max_output_tokens: .limits.max_output_tokens,
endpoint: .loom.endpoint
}'
A chat client selects deployments whose output includes text and whose
endpoint is /v1/chat/completions. Image and embedding integrations use
/v1/images/generations and /v1/embeddings respectively. The model ID and
limits come from this response; a checkpoint filename or architecture name is
not a substitute for the deployment contract.
loom-serve has no built-in authentication or TLS termination. Keep a direct
listener on a trusted network, or put an authenticated proxy or an SSH tunnel
in front of it. For example, this exposes a remote loopback listener only on
the client's loopback interface:
The client base URL is then http://127.0.0.1:18080/v1. Loopback inside a
container refers to that container. A container therefore needs host networking
or another hostname that reaches the host-side listener; publishing the Loom
server publicly is not a substitute for that routing.
Configure Pi with an explicit text model¶
Pi's generic OpenAI-compatible provider reads ~/.pi/agent/models.json. It
does not discover generic providers from /v1/models, so copy the selected
text deployment and its limits into that file. The following command prints a
complete configuration for one catalog row:
text_model=scout
curl --fail --silent --show-error "$loom_base_url/models" |
jq --arg base_url "$loom_base_url" --arg model "$text_model" '
(.data | map(select(.id == $model)) |
if length == 1 then .[0]
else error("expected exactly one catalog row for \($model)") end) as $row |
if ($row.output_modalities | index("text")) == null then
error("selected deployment does not produce text")
else {
"$schema": "https://pi.dev/schemas/models.schema.json",
"providers": {
"loom": {
"baseUrl": $base_url,
"api": "openai-completions",
"apiKey": "loom",
"compat": {
"supportsDeveloperRole": false,
"supportsReasoningEffort": false,
"maxTokensField": "max_completion_tokens"
},
"models": [{
"id": $row.id,
"name": $row.id,
"reasoning": false,
"input": ($row.input_modalities |
map(select(. == "text" or . == "image"))),
"contextWindow": $row.context_length,
"maxTokens": $row.limits.max_output_tokens,
"samplingParams": {"temperature": 0}
}]
}
}
} end'
Use the printed document as models.json for a new configuration. If the file
already contains providers, merge the loom member into its existing
providers object. The dummy API key makes the provider available to Pi; Loom
does not authenticate it. Opening Pi's /model picker reloads the file.
The compatibility block describes the current Loom chat profile. It keeps Pi
on the system instruction role because Loom's public profile does not promise
OpenAI's developer role; it selects max_completion_tokens and suppresses
OpenAI's reasoning_effort. Streaming usage, store:false and ordinary
function tools already match Pi's OpenAI-completions transport.
For a Qwen package whose source chat policy actually supports thinking, extend the model entry with:
Pi then sends the top-level enable_thinking boolean that Loom passes to source
policy. A model name alone does not establish that capability.
Pi's native llama.cpp integration is a different protocol. It owns router
discovery, load, unload and download operations that Loom does not expose. Use
the generic openai-completions provider above rather than configuring Loom as
a llama.cpp router.
Let Oh My Pi discover the mixed catalog¶
Oh My Pi can discover a generic OpenAI model list. Add this provider to
~/.omp/agent/models.yml:
providers:
loom:
baseUrl: http://127.0.0.1:8080/v1
auth: none
api: openai-completions
compat:
supportsDeveloperRole: false
supportsReasoningEffort: false
maxTokensField: max_completion_tokens
discovery:
type: openai-models-list
modelOverrides:
scout:
samplingParams:
temperature: 0
Change the URL and scout to match the client-visible server and selected text
deployment. omp models --kind all refreshes and displays every discovered
kind. Oh My Pi consumes Loom's existing modality and limit fields: text-output
rows become chat models, image-only rows become image runners and
embedding-only rows become embedding runners. The ordinary model picker remains
a chat-model view rather than presenting every server deployment as chat.
For a Qwen package that supports Loom's enable_thinking option, extend its
override with reasoning: true and this model-local compatibility setting:
Keep temperature: 0; the current Loom execution profile is greedy and rejects
nonzero temperature and other sampling controls.
Give Open WebUI one model for each operation¶
Open WebUI uses separate OpenAI settings for chat connections, image generation and RAG embeddings. Its chat discovery does not interpret Loom's modality fields, so explicitly allow only text deployments on the OpenAI connection. For a fresh Open WebUI data volume, these environment values establish a Loom-only configuration:
ENABLE_OLLAMA_API=false
ENABLE_OPENAI_API=true
OPENAI_API_BASE_URL=http://loom-host:8080/v1
OPENAI_API_KEY=loom
OPENAI_API_CONFIGS={"0":{"enable":true,"model_ids":["scout"]}}
DEFAULT_MODEL_METADATA={"capabilities":{"vision":false,"usage":true}}
ENABLE_IMAGE_GENERATION=true
IMAGE_GENERATION_ENGINE=openai
IMAGE_GENERATION_MODEL=illustrator
IMAGE_SIZE=1024x1024
IMAGES_OPENAI_API_BASE_URL=http://loom-host:8080/v1
IMAGES_OPENAI_API_KEY=loom
RAG_EMBEDDING_ENGINE=openai
RAG_EMBEDDING_MODEL=vectors
RAG_OPENAI_API_BASE_URL=http://loom-host:8080/v1
RAG_OPENAI_API_KEY=loom
Replace loom-host, the three model IDs, and the default image dimensions with
your deployment values. Open WebUI can request other sizes supported by the
image model; Loom specializes them on demand and reuses resident weights.
Omit the image or RAG block when no deployment provides that operation;
Open WebUI can retain its local
embedding engine instead.
OPENAI_API_CONFIGS key 0 describes the first OpenAI base URL. Its
model_ids list is the chat allowlist: illustrator and vectors remain
available to their operation-specific integrations without appearing in the
chat selector. The dummy keys satisfy Open WebUI's provider configuration and
are not an authentication boundary.
The shown default metadata is suitable for a Loom-only installation with a
text-only chat model. It prevents image attachments from being presented as a
model input and asks Open WebUI to retain streamed usage. If the selected text
deployment accepts image input, set vision to true. If one Open WebUI
instance has chat models with different capabilities, configure each model in
Open WebUI's workspace instead of applying one global default.
Open WebUI persists administrator changes. On an existing data volume, set the same values in its OpenAI connection, Images and Documents settings; a stored value takes precedence over a changed environment default. Keep nonzero sampling controls disabled in model settings.
Speech clients¶
A client that accepts raw audio can use POST /v1/audio/speech directly with an
explicit response_format:"pcm". Each response is a continuous stream of
signed little-endian PCM16 at 24 kHz, mono. The
speech walkthrough shows a
complete request and a streaming player. It also explains independent requests,
backpressure and cancellation. Available voice IDs and conditioning behavior
belong to the selected package.
Open WebUI's ordinary TTS adapter
awaits the complete provider response before returning a cached audio file.
Its full installation transcodes Loom's
audio/pcm;rate=24000;channels=1 response to browser-playable MP3 using FFmpeg.
For a prepared speech deployment named speaker with a voice named alba,
set these values under Admin Settings → Audio → Text-to-Speech:
| Setting | Value |
|---|---|
| Engine | OpenAI |
| API Base URL | Your server URL followed by /v1 |
| API Key | local, or the key required by your deployment proxy |
| Model | speaker |
| Voice | alba |
| Additional Parameters | {"response_format":"pcm"} |
The voice field accepts a package's voice ID directly. Choose a voice the selected deployment accepts; OpenAI's built-in voice names are not aliases for package voices. The additional format parameter is required because the speech endpoint accepts explicit PCM requests.
This path requires the full Open WebUI installation with FFmpeg available and
BYPASS_PYDUB_PREPROCESSING=false. Slim installations reject raw PCM, and
bypassing preprocessing leaves a headerless file that browsers cannot play.
The adapter buffers each complete provider response before playback, so
incremental server output does not start playback within that request.
Its realtime voice path additionally needs session events, microphone input, transcription, turn detection and interruption. Configuring a speech model on the ordinary OpenAI chat connection does not supply those capabilities.
Transcription and dictation¶
A client can send a completed recording to a recognition deployment through
POST /v1/audio/transcriptions and read the JSON text result or request plain
text. The transcription profile gives
a copyable curl request and accepted fields.
With audio codecs enabled, upload WAV, MP3, or FLAC directly; the server
converts sample rate and channel layout to the model's requirements. A PCM-only
build accepts signed PCM16LE with explicit rate and channels in the file
part's Content-Type. It converts the sample rate; channels must match the model.
Changing the filename or MIME label does not convert unsupported bytes into an
accepted format.
For a prepared recognition deployment named recognizer, set these values
under Admin Settings → Audio → Speech-to-Text in Open WebUI:
| Setting | Value |
|---|---|
| Engine | OpenAI |
| API Base URL | Your server URL followed by /v1 |
| API Key | local, or the key required by your deployment proxy |
| STT Model | recognizer |
| Request Format | Multipart Upload |
Open WebUI's transcription adapter
sends multipart audio and reads the returned text. Its full installation
uses FFmpeg to prepare browser recordings as MP3 before forwarding them. Keep
BYPASS_PYDUB_PREPROCESSING=false and enable MP3 input in the Loom server.
The server itself does not require FFmpeg or Python. This configures the
dictation microphone; read-aloud uses the separate TTS settings above.
Chunked multipart transfer is accepted, but recognition begins after the upload completes. The endpoint returns finalized text; appendable microphone sessions, provisional transcripts, turn detection, and interruption require a realtime session protocol in addition to this upload endpoint.
Diagnose the boundary¶
Test from the same network namespace as the client. A successful request from the host does not prove that a container can reach the URL:
curl --fail --silent --show-error "$loom_server_url/readyz"
curl --fail --silent --show-error "$loom_base_url/models"
Then test the operation directly with the exact model ID. The
HTTP profile reference gives accepted request
fields for chat, image, embedding, speech and transcription operations. Loom
deliberately rejects unknown or unsupported options instead of silently changing
their meaning.
Common configuration failures are a nonzero sampling parameter, an image size
unsupported by the model's source, an operation using a model of another kind,
or a client base URL that duplicates or omits /v1.