STANDIN_ variable. Every one of them can be overridden in code, because a constructor option always
beats an environment value.
Every variable the SDK reads
Two of these names are exported as constants rather than written out, so your code and the SDK cannot
drift apart:
OPENAI_,
ELEVENLABS_, DEEPGRAM_, CARTESIA_ or LIVEKIT_, and each plugin page lists its own. That split
is deliberate: a STANDIN_ variable configures your worker, never the provider you chose to answer
calls with. The three STANDIN_VISION_ variables sit on the line because the endpoint they name is
yours to pick, so keep in mind that STANDIN_VISION_API_KEY is a credential and belongs wherever the
rest of your secrets live.
The Python SDK reads the same fifteen, plus one this SDK has no use for.
STANDIN_SHOW_ROOTS fences
the document renderer that Python ships and TypeScript does not, so do not go looking for it here.
See Configuration for that side.How a value is read
The rule is the same everywhere: an explicit option wins, then the environment, then the documented default.A blank STANDIN_CHAT_SECRET does not fall through
A blank STANDIN_CHAT_SECRET does not fall through
ChatChannel reads the option, then STANDIN_CHAT_SECRET, then STANDIN_SECRET. That chain stops at
the first value that is present, and an exported empty string is present. So
export STANDIN_CHAT_SECRET= shadows a perfectly good STANDIN_SECRET and the channel throws at
construction instead. Unset the variable rather than blanking it.STANDIN_PORT is coerced, not validated
STANDIN_PORT is coerced, not validated
The port is read as
Number(process.env.STANDIN_PORT ?? 9442). A value that is not a number becomes
NaN rather than an error naming the variable, and you find out at start(). If you templated that
value from somewhere, check it before you pass it.An empty STANDIN_WS_PATH throws, and that is on purpose
An empty STANDIN_WS_PATH throws, and that is on purpose
The path is trimmed and its slashes normalized, and a path that reduces to
/ is refused with
wsPath must be a real path such as /msteams/calling. A worker listening at the root would answer
anything that reached it, so this fails at construction rather than at the first call.Reading your own configuration
Every plugin needs the same things: a value that must be set, one that may be, a boolean, a check that a vendor host is really that vendor’s, and a header map. When each plugin wrote its own, the error a user saw for a missing key depended on which provider they happened to pick. These five helpers are the shared version.purpose completes the sentence “X is required to …”, so write it as a verb phrase and the message
reads properly:
flag is strict on purpose. The value is trimmed and lowercased, then compared to the single word
true, so TRUE and True work and 1, yes, on and y are all false. Most flags worth having
turn a guard off, and a guard must not be disabled by a plausible-looking typo landing in a config
file. A reader who wanted it on and typed 1 gets the safe direction and a behaviour they will
notice, not a guard quietly removed. An unset variable is not a typo, so it returns the fallback
untouched rather than being forced to false.
vendorHost is the one worth reading twice. Your API key travels to whatever host the
configuration names, so a mistyped or injected host is not a failed call, it is credential
exfiltration. That is why it throws rather than warning:
jsonObject never logs the value it rejected. It is used for header maps, and a header map
carries your credentials to somebody else’s endpoint. The error says the variable must be a JSON
object and stops there.
These five live at the package barrel in TypeScript, so
import { required } from "@komaa/standin-sdk"
is the whole import. The Python SDK does not re-export them: they are standin.config.required,
standin.config.optional, standin.config.flag, standin.config.vendor_host and
standin.config.json_object, imported as from standin.config import required. Same behaviour, same
argument order, different import line.Optional peer dependencies
The core installs with one runtime dependency,ws, and needs Node 20 or newer. That floor is the
core’s floor, not the highest any plugin wants, so a plugin that needs more says so on its own
page. Everything else this package can reach is an optional peer, which is what lets one package ship
every plugin: the core imports nothing from plugins/, so the bare specifier works on a machine with
no framework installed at all.
How a missing one surfaces
LiveKit is loaded at use time rather than at module load, so importing the plugin on a machine without LiveKit still succeeds and the failure, when it comes, arrives translated:Cannot find module '@livekit/rtc-node' thrown out
of somebody else’s package cannot tell a missing optional dependency from a broken install, and will
go looking in the wrong place. Naming the install line ends it in one line.
sharp is the other shape. It is looked for, and when it is not there the SDK warns and carries on:
encoder on
TileStreamOptions and sharp is never looked for at all.
The two SDKs also do not have the same optional set, because the frameworks are not the same in each
language. TypeScript needs two LiveKit packages and uses sharp to encode tile frames. Python reaches
LiveKit through one extra, standin-sdk[livekit], encodes the same frames with Pillow from
standin-sdk[tile], and carries a render extra for the document renderer that has no counterpart
here. Read the peers of the SDK you are installing, not the other one’s.
Version
VERSION is this package’s own version, the same string the
Python twin exposes as standin.__version__. SCHEMA_VERSION is the chat wire schema, versioned
separately and bumped only for a breaking change, because the schema already requires a receiver to
ignore fields it does not know. Knowing both is what separates “your worker is old” from “the message
was wrong”.
Next
CallServer
The listener most of these variables configure.
Checking the install
Prove the configuration is right before a real call does.