Skip to main content

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/branch arms and v2.36.0 interrupt bodies/handlers) must have exactly one map: rule of the right direction; inbound JSON rules must have distinct discriminators; at most one receive … as binary rule. A message that would silently fall through untranscoded is a compile error, not a runtime surprise.
  • axon-T850 — config, not code. resolve: and secret: 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 declared session and role: one of its two roles. With backpressure: 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 key f (Gemini Live marks frame kinds by which key exists). Equality rules dispatch before presence rules.
  • A frame matching no rule surfaces as an explicit Unmapped event — 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.