upstream
Since v2.37.0 · Top-level declaration
Grammar
upstream <Name> [from <Preset>@v<N>] {
transport: websocket
protocol: <SessionName>
role: <RoleName>
resolve: <config.key> # per-tenant URL key — never a literal
secret: <config.key> # per-tenant credential key — never a literal
auth: header("<Name>"[, "<Prefix>"]) | query("<param>") | signed_url
map: [
send <Type> as json [tag "<Tag>"] | send <Type> as binary,
receive <Type> as json [when "<field>" [= "<value>"]] | receive <Type> as binary,
...
]
reconnect: { backoff_ms: <int>, max_attempts: <int>, on_exhausted: fail }
overflow: drop_oldest | degrade_quality | pause_upstream | fail
backpressure: credit(<n>)
}
upstream declares a persistent, config-resolved, outbound
connection to a third-party service — a streaming STT vendor, a
TTS vendor, or a fused speech-to-speech API. It is the dual
transport role of socket: a socket is axon acting as
server (the carrier or browser dials in), an upstream is
axon acting as client (axon dials out). Both bind the same
v2.3.0 session algebra — one algebra, two transport roles.
The doctrine is voice_integration_is_a_declaration_not_a_rewrite:
every competing stack ties orchestration code to one vendor's
SDK, so swapping Deepgram for AssemblyAI — or a cascaded
STT→LLM→TTS pipeline for a fused realtime API — means rewriting
integration code. In axon the vendor is a declaration: the
.axon source names a session, a role, and a wire projection;
the runtime does the plumbing. Changing vendors is a config
edit, the same property tool { provider: http } gave REST
(v2.8.0), extended to persistent bidirectional streams.
Surface
upstream is a top-level declaration. Its protocol:
references a declared session (exactly like socket); role:
names which side of that dialogue axon plays. The peer role is
the vendor's — realised by the map: transcoding, never by
axon code.
session SttDialogue {
axon: [ send AudioChunk, receive Transcript, loop ]
vendor: [ receive AudioChunk, send Transcript, loop ]
}
upstream DeepgramSTT {
transport: websocket
protocol: SttDialogue
role: axon
resolve: upstream.deepgram.url
secret: upstream.deepgram.api_key
auth: header("Authorization", "Token ")
map: [
send AudioChunk as binary,
receive Transcript as json when "type" = "Results",
]
reconnect: { backoff_ms: 500, max_attempts: 5, on_exhausted: fail }
overflow: drop_oldest
}
The laws the compiler enforces
- axon-T849 — projection totality. Every message the bound
role sends or receives (including inside
select/brancharms and v2.36.0interruptbodies/handlers) must have exactly onemap:rule of the right direction; inbound JSON rules must have distinct discriminators; at most onereceive … as binaryrule. A message that would silently fall through untranscoded is a compile error, not a runtime surprise. - axon-T850 — config, not code.
resolve:andsecret:are per-tenant config keys (lowercase, dot-separated — the compile-time mirror of the enterprise secret-key policy). A URL or credential literal in source cannot compile. - axon-T851 — session/role binding.
protocol:must be a declaredsessionandrole:one of its two roles. Withbackpressure: credit(n), the v2.3.0 Presburger discharge runs on the bound role.
Projection semantics (the two shapes every 2026 vendor uses)
send M as json— the payload JSON verbatim: the flow builds the vendor's exact wire shape (ElevenLabs{"text": …}).send M as json tag "X"— the payload object with"type": "X"injected (the Deepgram-control / OpenAI-Realtime family).receive M as json when "f" = "v"— equality discriminator (default:"type" = "M").receive M as json when "f"— presence discriminator: the frame HAS keyf(Gemini Live marks frame kinds by which key exists). Equality rules dispatch before presence rules.- A frame matching no rule surfaces as an explicit
Unmappedevent — narrowing the projection to what you consume is legitimate; silence is not.
Inbound payloads are Json
The projection deliberately does not ship a structural
field-mapping language. An inbound vendor frame lands as the
session message's payload typed Json (v2.26.0) with total
navigation — Transcript.payload.channel.alternatives[0].transcript
— miss ⇒ null, never a crash. The session message name is the
routing and duality skeleton; the open Json type absorbs
vendor-shape variance without a rewrite.
Presets
Blessed vendors ship as versioned stdlib presets instantiated
with from:
upstream MySTT from DeepgramSTT@v1 {
secret: upstream.deepgram.api_key
}
A preset is an ordinary upstream under the hood — axon desugar prints the exact expanded declaration, and forking it
into a local hand-written upstream is always available.
The honest limit
Duality, credit discipline, and projection totality are
compiler-proved up to the wire. Axon does not — cannot —
prove the vendor's own flow-control sound (code axon does not
own). The runtime defends (overflow: policy, fail-closed
reconnect:) and the enterprise layer witnesses every
lifecycle transition (upstream:connected / :reconnected /
:exhausted, fail-closed audit); neither claims a proof across
the trust boundary.