Skip to content

Relay API reference: TypeSpec→AsyncAPI 3.0 tooling + full Relay surface + Fern docs#423

Draft
Devon-White wants to merge 74 commits into
mainfrom
devon/relay-asyncapi-tooling
Draft

Relay API reference: TypeSpec→AsyncAPI 3.0 tooling + full Relay surface + Fern docs#423
Devon-White wants to merge 74 commits into
mainfrom
devon/relay-asyncapi-tooling

Conversation

@Devon-White

Copy link
Copy Markdown
Collaborator

Description

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation update
  • Code cleanup / refactor

Related Issues

Testing

  • Added/updated unit tests
  • Tested manually
  • Tested with live SignalWire credentials (if applicable)

Checklist

  • I have read the CONTRIBUTING guidelines
  • My code follows the project's style guidelines
  • I have added tests for my changes (if applicable)
  • I have updated documentation (if applicable)
  • All existing tests pass

Additional Notes

Also fixes test harness: import the library and use the full
SignalWire.AsyncAPI namespace path so decorators resolve.
…uth security

Avoids depending on @typespec/http (whose lib tsp won't load in the
hoisted standalone tester); custom @bearerAuth keeps the emitter self-contained.
…dModel, inline-param docs

Addresses adversarial review: numeric enum/discriminator type inference (latent
invalid-schema bug), Array.isArray guard, isNamedModel helper, inline-parameter
description extraction, and a multi-method/multi-event collision-coverage test.
String-discriminator output is unchanged (golden snapshot + Relay artifact in sync).
@minValue/@maxValue/exclusive, @minLength/@maxlength, @minItems/@Maxitems,
@pattern, @Format, @secret→format:password, property defaults, and @example
now map to the corresponding JSON-Schema keywords (valid per AsyncAPI 3.0's
Draft-07 superset). Relay timeout now models its non-negative/default-30 semantics.
…odedname

Completes the standard property-decorator surface: @encode decays to its wire
type with a JSON-Schema format (e.g. utcDateTime+rfc3339 → string/date-time),
@Encodedname remaps the emitted JSON property key. No standard property decorator
is silently dropped now.
…r assertions, dup-method diagnostic

Addresses the remaining adversarial-review quality findings:
- Replace SchemaObject=Record<string,unknown> with a structured AsyncAPISchema
  interface (correct AsyncAPI/Draft-07 dialect: numeric exclusiveMin/Max, string
  discriminator, no nullable) — removes the as-Record casts. Not reusing
  @typespec/openapi3's OpenAPI3Schema, which models a different dialect.
- RefFn returns SchemaOrRef (= AsyncAPISchema | AsyncAPIRef); unionInline typed
  to Union (removes the as-any casts).
- emitRpcMethods/emitEvents take a typed EmitTarget context instead of drilling
  doc!.components! — removes non-null assertions.
- Add duplicate-@rpcMethod diagnostic + test.
…p invented /api/relay/wss path)

Verified against the TS + Python SDK clients: both connect to `wss://${host}` with
DEFAULT_RELAY_HOST = relay.signalwire.com and reject a host containing a path. The
service is selected by the JSON-RPC method in the payload, not a URL path.
With address null, Fern composes the channel URL as wss://<host>/<channelId>,
showing a misleading per-service path (e.g. wss://relay.signalwire.com/messaging).
Relay multiplexes every service over one root connection and routes by the JSON-RPC
method, so emit the root address "/" — Fern then renders wss://relay.signalwire.com/.
Verified live for the messaging and calling channels.
…pecs

The emitter's dist/ is gitignored and nothing built it in CI, so build:relay
failed with import-not-found on ../dist/src/index.js. build:relay now runs the
emitter's tsc build first (verified against a clean dist/).
The Relay API was rendering in the same flat sidebar as REST. Convert the apis
product nav to tabs (matching the compatibility-api pattern): a REST tab and a
Relay tab. URLs are preserved — Core stays at /docs/apis/<page>, REST at
/docs/apis/rest/..., Relay at /docs/apis/relay/... (tab carries the slug, the API
uses skip-slug). Verified live: both tabs render and all existing URLs 200.
…xample

Emitter now reads @opExample on RPC ops (parameters -> request frame,
returnType -> reply frame) and @example on event models (-> signalwire.event
carrier frame), emitting them as AsyncAPI message examples. Adds examples.test.ts.
…mples

Correctness fixes from the one-to-one spec audit (6 services / 71 methods / 26 events):
- pay: input enum speech->voice; add PayPrompt.attempt + require_matching_inputs;
  actions optional; numeric/bool params retyped to wire strings per pay_schema.md
- calling: join_conference recording_status_callback_event enum->string;
  hold/unhold result state literals; broaden TapCodec + detector-event enums
- signalwire: ConnectResult.ice_servers optional; strip internal jargon (BLADE/etc.)
- tasking: drop carrier fields (timestamp/space_id/project_id) the emitter synthesizes
- add missing customer-facing @doc across event/refer models
Examples: @opExample on every method + @example on every event; discriminator-union
variant models carry schema-level @example where an op frame can't narrow the base.
All 6 specs validate 0 errors via the official AsyncAPI CLI; fern check clean.
…_transcribe/translate

Verified the SWML models against the mod_openai/mod_infrastructure C engine
(96/96 AIParams fields parsed by the engine; SWAIG schema shared via one
process_swaig_function; live_transcribe/translate hit the same parse_transcribe_json).
Replicated the engine-confirmed shapes into the Relay specs (cross-import blocked by
@jsonSchema/SWMLVar), stripping ~72 markup arms:
- calling.ai/amazon_bedrock: params Record<unknown> -> typed AiBehaviorParams (91 fields)
- SWAIG: native_functions enum, web_hook_auth_* + meta_data on function/defaults/includes,
  mcp_servers; kept parameters/data_map/internal_fillers loose (@OneOf, validate-first)
- live_transcribe/translate: start/summarize/inject Record<unknown> -> typed action models
- added @opExample to all 7 AI ops
calling validates 0 errors (AsyncAPI CLI); fern check clean.
Replace the hand-replicated AiBehaviorParams (91 fields) with a direct spread of
the engine-verified SWML AIParams model, stripping its markup-only `| SWMLVar`
template-variable arms at emit time so they never reach the live JSON-RPC wire.

- typespec-emit-filter: standalone, generic @excludeFromEmit(...types) decorator
  package (TypeSpec's shared-decorator-library pattern).
- asyncapi emitter: honor @excludeFromEmit on models/unions/properties — drop the
  named arms and collapse the union (0 -> {}, 1 -> bare, all-string -> enum, else
  oneOf). Applied on both the component and inline-model paths (open models with a
  `...Record<unknown>` index signature always inline).
- asyncapi emitter: namespace-qualified component names via getTypeName so reused
  cross-namespace SWML types stay collision-free (service-local types stay bare);
  add a duplicate-type-name diagnostic backstop.
- relay calling: AiBehaviorParams now spreads SWML.Calling.AIParams, keeping the 2
  deprecated eleven_labs_* fields.

Verified: 32 emitter tests pass; AsyncAPI CLI 0 errors; fern check clean (only the
known FDR 403); parity diff vs 158d9c1 shows only additive/authoritative deltas
(added defaults/examples, SWML constraints, reused SWML.Calling.ConversationMessage,
+2 deprecated fields, int32->integer format drops); the other 5 relay specs are
byte-identical; SWML's own json-schema emit still includes SWMLVar.
…zon_bedrock

Extend the SWML reuse from `params` (AIParams) to the entire AI agent config object,
which mod_openai's create_app_from_json parses identically for the SWML `ai` verb and
Relay calling.ai (verified in C source). The Relay-local Ai* sub-models were incomplete
projections of one shared backend object; replace them with direct references to the
engine-verified SWML models.

- AiParams/AmazonBedrockParams: prompt -> AIPrompt, post_prompt -> AIPostPrompt,
  pronounce -> Pronounce, hints -> (string|Hint)[], languages -> Languages,
  SWAIG -> SWAIG (incl. the full function/parameters/data_map tree and the SWMLAction
  that returns a full SWML document), global_data -> GlobalData. Delete the 11 hand-rolled
  Ai* sub-models (-288 lines net).
- Exclude ONLY the markup-only `SWMLVar` template-variable arms via @excludeFromEmit on
  the entry models (resolved at SWML execution, never on the live JSON-RPC wire). Every
  other surface SWML accepts is emitted as-is, including the recursive SWML document a
  SWAIG data_map action can return (handled natively via $ref).
- Reused SWML as-is; SWML's current gaps vs Relay (missing web_hook_auth_* on SWAIG,
  Hint requiring pattern/replace, AIPrompt missing barge_confidence/model) are logged in
  SWML-REUSE-GAPS.md for a later SWML-truing pass.

Verified: 0 SWMLVar leak; valid AsyncAPI (fern check clean); fern docs dev renders the
calling AI reference incl. the recursive SWML `$ref`; other 5 relay specs byte-identical;
SWML's own emit unaffected.
Devon-White and others added 30 commits June 22, 2026 17:20
…en) on signalwire.connect; drop inaccurate HTTP bearer scheme; note per-service client/server audience
- Fix unterminated doc code span in conference status_callback_event
- Reword @server description to drop in-band/transport jargon
- Regenerate relay.yaml and relay-single.yaml from updated specs
Replace @rpcMethod + @channelPerCommand with a single per-operation @channel
decorator: each operation gets its own root-addressed AsyncAPI 3.0 channel.
Drop @channelPerCommand; channel-mode is now multi|single. Server-pushed events
not returned by any op get their own receive-only channel (uniform output).

Verified: relay-single.yaml byte-identical; relay.yaml components (schemas +
messages), signalwire, and calling channels/operations byte-identical; 243
operations preserved; only messaging/tasking/provisioning/webrtc regroup into
per-op + per-event channels. Emitter tests 50/50 green.
…ire/tasking/provisioning/webrtc (Part B, 5/6 services)

Reorganize each non-calling service into <service>/main.tsp + models/core.tsp +
<feature>/main.tsp + <feature>/models/{requests,responses}.tsp + events/<event>.tsp.
Output byte-identical (relay.yaml + relay-single.yaml unchanged).
…6/6 services)

Split the 8 grab-bag method files + common.tsp + media-1/media-2 event files into
34 per-operation feature directories (<feature>/main.tsp + models/{requests,responses}.tsp),
shared models/, and meaningfully-named events/ files. Operation declaration order
preserved; relay.yaml + relay-single.yaml byte-identical.
Convert every /** */ doc comment across the Relay specs to @doc(...) for
consistency with the REST/JSON-Schema specs. Multi-line uses triple-quoted @doc;
single-line escapes internal quotes. Output byte-identical.
5 responses.tsp files spread ...Result without importing their service's
models/core.tsp; signalwire/receive referenced Acknowledgement and
Relay.Calling.CallReceiveEvent without imports. These resolved only via the root
main.tsp's transitive imports, so the language server flagged them per-file.
Add the direct imports; every spec file now compiles standalone with zero
invalid-ref errors. Output byte-identical.
…elative import paths

Move every operation feature directory into <service>/operations/, leaving
models/ (shared) and events/ (received events) cleanly separated at the service
top level. Update main.tsp feature imports and the relative paths in moved files
(service-level models/events and cross-service/swml refs gain one ../).
Output byte-identical; every file compiles standalone with zero invalid-ref.
Source-verified against mod_infrastructure (C), mod_openai (C), and prime-rails
(Ruby). Params-level type/enum/required/missing-param/event-field/example fixes,
e.g.: SipCodec +AMR-WB, ToneName +10 codes, ConferenceRegion +ch,
ConferenceCallbackEventType +laml, TapCodec closed enum, tap direction required,
record.audio +max_length, collect +model, missing dial/connect/answer scalars,
event fields (direction/end_reason/failed_reason/recording_id/...), and corrected
@opExample reply strings. Layering-sensitive findings (node_id envelope, fabric
naming, wire method names) deferred as FLAG. Build + 50/50 emitter tests +
AsyncAPI conformance pass.
Source-wins fixes: receive event call_state uses real CallState values (drop
phantom ReceiveCallState); remove never-emitted created_by; messaging
ReceiveEvent.media nullable; drop never-emitted error variants from detect/fax
events. Result envelope: add call_id?/control_id? to shared RelayResult (dedup 14
results). Add source-confirmed customer params: conference acl, SIP encryption,
typed confirm (url|object), queue wait_url/wait_time/execute_after_queue/whisper_url.
Build + dangling-ref scan + AsyncAPI conformance pass.
…ice_id)

Resolved open FLAGs via prime-rails + customer-SDK source:
- Envelope (node_id) and user_event(event:string) CONFIRMED correct via SDKs — no change.
- Conference event (built in C, tapped by prime-rails): add record-*/stream-* statuses,
  region/size/call_id_to_coach fields, node_id optional.
- Messaging: backend emits tags:[] (fix examples); add 'read' delivery state.
- Bedrock voice_id moved from prompt to AmazonBedrockObject root with the real
  5-voice enum + tiffany default (bedrock.c:5511,129-136); fixes silent-ignore bug
  for SWML + Relay reuse. Build:relay + build:schema + scan + 50/50 tests pass.
…it/source-verified)

Git history (commit #669 deleted old conference_controller.c, added
new_conference_controller.c; recent commits touch only the new one) confirms the
live controller emits start_on_join/end_on_leave/call_id_ending_conf
(new_conference_controller.c:688/689/497) — the spec's start_on_enter/end_on_exit/
call_ending_conference came from stale docs. Removed phantom participant_call_status
and reason_participant_left (emitted nowhere in source). Source > docs.
The spec modeled the post-gateway blade method 'message' (copied from a doc that
states 'all methods are blade.execute'). The customer-facing v4 browser SDK sends
'webrtc.verto' with a required callID and reads {code, result, node_id}
(webrtc.c:14779 = node 'message'; v4 SDK = webrtc.verto). Rename @channel to
webrtc.verto, add required callID, reshape MessageResult to {code, result?,
node_id?}. Build + scan pass.
Verified against @signalwire/realtime-api@4.2.1: Task.send() POSTs to
/api/relay/rest/tasks via node:https (HTTP 204) — there is no WebSocket send
method. Drop the deliver operation (it belongs in the REST spec) and keep only
the inbound queuing.relay.tasks event in this AsyncAPI spec; namespace doc now
points delivery to the REST endpoint. Removed unused Result model.
Verified: no SDK sends conference.list (v4 Call Fabric + typescript-web + realtime
Node all have zero references; v4 lists fabric addresses, not conferences), and C
does not register it as an RPC — webrtc.c:14779 registers only message/detach/
reattach/bootstrap. The only conference.list in C is a Kafka event 'source' tag
(conference_events.c:907). The spec had modeled an internal blade.execute/event
artifact as a request/reply op (same provenance error as message). Removed the
operation + ConferenceListParams/Conference/ConferenceListResult + unused Result.
Build + scan + 50/50 tests pass.
Remove the 'Relay (Single Channel)' tab and the 'Playground' page (which rendered
relay-single) from the Relay tab. Also fix the Webrtc section's stale
referenced-packages (webrtc/message/conferenceList -> webrtcVerto/webrtcMessage)
left over from the webrtc.verto rename + conference.list removal.
The relay-single (single-channel) spec is no longer used in the docs (tab +
Playground page removed). Drop its generation entirely: remove the
build:relay-single-spec step from build:relay, delete specs/relay/tspconfig.single.yaml
and the generated fern/apis/relay-single/ output. Only the one multi-channel Relay
AsyncAPI spec (relay.yaml) remains. The emitter's channel-mode:single capability and
its unit test are unchanged (still a supported feature, just unused by the build).
…nhance event models

- Introduced JsonRpcRequest and JsonRpcResponse models for standardized request/response handling across Relay services.
- Updated transfer and user-event operations to utilize new request/response models.
- Refactored messaging events (receive, state) to align with new event model structure.
- Enhanced connect and disconnect operations with new request/response models.
- Added new documentation pages for Relay authentication and error handling.
- Improved overall structure and clarity of Relay API specifications.
- emitter: a parameterless @channel op now emits a receive-only channel
  (channel + receive ops, no request frame, no send op); add the
  reply-without-request diagnostic; correct the stale channel-mode doc
- tasking: model queuing.relay.tasks as a receive-only channel and move
  TasksEvent off signalwire.receive (de-dup) with a pointer note on receive
- reorg every operation to models/{send,reply,events}.tsp (REST-aligned,
  AsyncAPI-named); split state.tsp into per-op events plus
  calling/events/shared.tsp; co-locate every per-op event with its op
- apis.yml: fix relay nav refs (messagingSend, provisioningConfigure,
  queuingRelayTasks; drop invalid tasking/webrtcMessage)
- rebuild all specs

The reorg is output-neutral (relay.yaml byte-identical).
- Removed redundant comments and documentation sections across various models in the Relay Calling API specifications to enhance clarity and reduce clutter.
- Streamlined model definitions by eliminating unnecessary comments related to shared structures and enums.
- Updated event and operation models to focus on essential documentation, improving readability and maintainability.
- Ensured consistency in documentation style across all models, enhancing the overall coherence of the API specifications.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[DOC] - Document Relay APIs

1 participant