{"id":26271754,"url":"https://github.com/agrathwohl/t140llm","last_synced_at":"2026-02-25T08:17:05.377Z","repository":{"id":280311829,"uuid":"941579907","full_name":"agrathwohl/t140llm","owner":"agrathwohl","description":"Deliver streaming LLM text over a T.140 RTP payload","archived":false,"fork":false,"pushed_at":"2026-02-23T20:58:19.000Z","size":2712,"stargazers_count":8,"open_issues_count":4,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2026-02-24T03:15:35.904Z","etag":null,"topics":["forward-error-correction","llm","rfc-4103","rtp","sip","sockets","srtp","streaming","t140","text","udp","webrtc"],"latest_commit_sha":null,"homepage":"https://agrathwohl.github.io/t140llm/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/agrathwohl.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-03-02T16:20:32.000Z","updated_at":"2026-02-23T20:58:22.000Z","dependencies_parsed_at":"2025-03-02T17:32:44.920Z","dependency_job_id":"0fa1b777-e002-4cfd-92db-1f9e4909fc7f","html_url":"https://github.com/agrathwohl/t140llm","commit_stats":null,"previous_names":["agrathwohl/t140llm"],"tags_count":18,"template":false,"template_full_name":null,"purl":"pkg:github/agrathwohl/t140llm","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/agrathwohl%2Ft140llm","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/agrathwohl%2Ft140llm/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/agrathwohl%2Ft140llm/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/agrathwohl%2Ft140llm/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/agrathwohl","download_url":"https://codeload.github.com/agrathwohl/t140llm/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/agrathwohl%2Ft140llm/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29815020,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-25T05:36:42.804Z","status":"ssl_error","status_checked_at":"2026-02-25T05:36:31.934Z","response_time":61,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["forward-error-correction","llm","rfc-4103","rtp","sip","sockets","srtp","streaming","t140","text","udp","webrtc"],"created_at":"2025-03-14T07:14:34.415Z","updated_at":"2026-02-25T08:17:05.364Z","avatar_url":"https://github.com/agrathwohl.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\" style=\"text-align: center;\"\u003e\n\n  ![T140LLM](logo.gif)\n\n  \u003ch1 style=\"border-bottom: none;\"\u003et140llm\u003c/h1\u003e\n\n  [![npm version](https://img.shields.io/npm/v/t140llm)](https://www.npmjs.com/package/t140llm)\n  [![npm downloads](https://img.shields.io/npm/dm/t140llm)](https://www.npmjs.com/package/t140llm)\n\n  \u003cp\u003eA TypeScript library to convert LLM streaming responses into T.140 real-time text format for SIP, WebRTC and (S)RTP applications\u003c/p\u003e\n\u003c/div\u003e\n\n\u003chr /\u003e\n\n\u003e Convert LLM streaming responses into T.140 real-time text\n\n## Table of contents \u003c!-- omit in toc --\u003e\n\n- [Pre-requisites](#pre-requisites)\n- [Setup](#setup)\n  - [Install](#install)\n  - [Usage](#usage)\n    - [Basic Usage](#basic-usage)\n    - [With Vercel AI SDK](#with-vercel-ai-sdk)\n    - [With Anthropic Claude](#with-anthropic-claude)\n    - [Direct RTP Streaming](#direct-rtp-streaming)\n    - [Secure SRTP Streaming](#secure-srtp-streaming)\n    - [With Forward Error Correction](#with-forward-error-correction)\n    - [With Custom Transport](#with-custom-transport)\n    - [Pre-connecting to Transport](#pre-connecting-to-transport)\n    - [Multiplexing Multiple LLM Streams](#multiplexing-multiple-llm-streams)\n- [How It Works](#how-it-works)\n- [API Reference](#api-reference)\n  - [processAIStream(stream, [websocketUrl])](#processaistreamstream-websocketurl)\n  - [processAIStreamToRtp(stream, remoteAddress, [remotePort], [rtpConfig])](#processaistreamtortpstream-remoteaddress-remoteport-rtpconfig)\n  - [processAIStreamToSrtp(stream, remoteAddress, [remotePort], srtpConfig)](#processaistreamtosrtpstream-remoteaddress-remoteport-srtpconfig)\n  - [processAIStreamToDirectSocket(stream, [socketPath], [rtpConfig])](#processaistreamtodirectsocketstream-socketpath-rtpconfig)\n  - [processAIStreamsToMultiplexedRtp(streams, remoteAddress, [remotePort], [rtpConfig])](#processaistreamstomultiplexedrtpstreams-remoteaddress-remoteport-rtpconfig)\n  - [createT140WebSocketTransport(websocketUrl, [options])](#createt140websockettransportwebsocketurl-options)\n  - [createDirectSocketTransport(socketPath, [rtpConfig])](#createdirectsockettransportsocketpath-rtpconfig)\n  - [createT140RtpTransport(remoteAddress, [remotePort], [rtpConfig])](#createt140rtptransportremoteaddress-remoteport-rtpconfig)\n  - [createT140SrtpTransport(remoteAddress, [remotePort], srtpConfig)](#createt140srtptransportremoteaddress-remoteport-srtpconfig)\n  - [createT140RtpMultiplexer(remoteAddress, [remotePort], [multiplexConfig])](#createt140rtpmultiplexerremoteaddress-remoteport-multiplexconfig)\n  - [createRtpPacket(sequenceNumber, timestamp, payload, [options])](#creatertppacketsequencenumber-timestamp-payload-options)\n  - [createSrtpKeysFromPassphrase(passphrase)](#createsrtpkeysfrompassphrasepassphrase)\n  - [T140RtpTransport](#t140rtptransport)\n  - [T140RtpMultiplexer](#t140rtpmultiplexer)\n  - [T140StreamDemultiplexer](#t140streamdemultiplexer)\n  - [TransportStream Interface](#transportstream-interface)\n- [License](#license)\n\n## Pre-requisites\n\n- [Node.js][nodejs-url] \u003e= 16.0.0\n- [NPM][npm-url] \u003e= 6.13.4 ([NPM][npm-url] comes with [Node.js][nodejs-url] so there is no need to install separately.)\n\n## Setup\n\n### Install\n\n```sh\n# Install via NPM\n$ npm install --save t140llm\n```\n\n### Features\n\n- [x] T.140 RTP Payload Formatting\n- [x] T.140 redundancy\n- [x] T.140 FEC (forward error correction)\n- [x] (S)RTP Direct Delivery\n- [x] Customizable Rate Limiting and Token Pooling\n- [x] Custom Transport Streams (WebRTC, custom protocols, etc.)\n- [x] UNIX SEQPACKET sockets (for supporting \u003e1 LLM stream simultaneously)\n- [x] UNIX STREAM sockets (for single LLM stream support)\n- [x] WebSocket\n- [x] Stream Multiplexing (combine multiple LLM streams into a single RTP output)\n- [x] Direct AsyncIterable support (pass LLM SDK streams directly — no EventEmitter wrapping needed)\n\n### Support\n\n- [x] Vercel AI SDK\n- [x] Anthropic SDK\n- [x] OpenAI SDK\n- [x] Cohere\n- [x] Mistral\n- [ ] Amazon (Bedrock)\n- [x] Google (Gemini)\n- [x] Ollama\n- [x] Reasoning Support\n- [ ] Binary Data\n- [x] Tools\n- [x] LLM Output Metadata\n- [ ] PDFs/Documents\n- [ ] Images\n- [ ] Video\n- [ ] Signaling\n- [ ] Custom RTP Packet Data\n\n### Usage\n\nEver wanted to send an LLM text stream to a [telegraph machine][tm]? Or send Claude\nto an [assistive reader device][ad]? Or pipe some o1 reasoning directly to a [satelite\norbiting the planet][sat] with forward error correction to ensure the message\narrives in full? If so, read on...\n\n#### Basic Usage\n\n```typescript\nimport { processAIStream } from \"t140llm\";\nimport { OpenAI } from \"openai\";\n\n// Initialize your LLM client\nconst openai = new OpenAI({\n  apiKey: process.env.OPENAI_API_KEY,\n});\n\n// Create a streaming response\nconst stream = await openai.chat.completions.create({\n  model: \"gpt-4\",\n  messages: [{ role: \"user\", content: \"Write a short story.\" }],\n  stream: true,\n});\n\n// Process the stream and convert to T.140\nprocessAIStream(stream);\n```\n\n#### With Vercel AI SDK\n\n```typescript\nimport { processAIStream } from \"t140llm\";\nimport { StreamingTextResponse, Message } from \"ai\";\nimport { OpenAI } from \"openai\";\n\n// Initialize OpenAI client\nconst openai = new OpenAI({\n  apiKey: process.env.OPENAI_API_KEY,\n});\n\n// Example API route handler\nexport async function POST(req: Request) {\n  const { messages }: { messages: Message[] } = await req.json();\n\n  // Create a stream with the Vercel AI SDK\n  const response = await openai.chat.completions.create({\n    model: \"gpt-4\",\n    messages,\n    stream: true,\n  });\n\n  // Process the stream with t140llm\n  processAIStream(response);\n\n  // You can still return the response to the client\n  return new StreamingTextResponse(response);\n}\n```\n\n#### With Anthropic Claude\n\n```typescript\nimport { processAIStream } from \"t140llm\";\nimport Anthropic from \"@anthropic-ai/sdk\";\n\n// Initialize Anthropic client\nconst anthropic = new Anthropic({\n  apiKey: process.env.ANTHROPIC_API_KEY,\n});\n\n// Create a streaming response\nconst stream = await anthropic.messages.create({\n  model: \"claude-3-sonnet-20240229\",\n  messages: [{ role: \"user\", content: \"Write a short story.\" }],\n  stream: true,\n});\n\n// Process the stream and convert to T.140\nprocessAIStream(stream);\n```\n\n#### With Mistral AI\n\n```typescript\nimport { processAIStream } from \"t140llm\";\nimport MistralClient from \"@mistralai/mistralai\";\n\n// Initialize Mistral client\nconst mistral = new MistralClient({\n  apiKey: process.env.MISTRAL_API_KEY,\n});\n\n// Create a streaming response\nconst stream = await mistral.chat({\n  model: \"mistral-large-latest\",\n  messages: [{ role: \"user\", content: \"Write a short story.\" }],\n  stream: true,\n});\n\n// Process the stream and convert to T.140\nprocessAIStream(stream);\n```\n\n#### With Cohere\n\n```typescript\nimport { processAIStream } from \"t140llm\";\nimport { CohereClient } from \"cohere-ai\";\n\n// Initialize Cohere client\nconst cohere = new CohereClient({\n  token: process.env.COHERE_API_KEY,\n});\n\n// Create a streaming response\nconst stream = await cohere.chatStream({\n  model: \"command\",\n  message: \"Write a short story.\",\n});\n\n// Process the stream and convert to T.140\nprocessAIStream(stream);\n```\n\n#### With Google Gemini\n\n```typescript\nimport { processAIStream } from \"t140llm\";\nimport { GoogleGenerativeAI } from \"@google/generative-ai\";\nconst genAI = new GoogleGenerativeAI(process.env.GOOGLE_API_KEY);\nconst model = genAI.getGenerativeModel({ model: 'gemini-pro' });\nconst chat = model.startChat();\nconst result = await chat.sendMessageStream(\"Write a short story.\");\n// Pass the async iterable stream directly — no EventEmitter wrapper needed\nprocessAIStream(result.stream);\n```\n\n#### With Ollama\n\n```typescript\nimport { processAIStream } from \"t140llm\";\nimport { Ollama } from \"ollama\";\n\n// Initialize Ollama client\nconst ollama = new Ollama();\n\n// Create a streaming response\nconst stream = await ollama.chat({\n  model: \"llama3\",\n  messages: [{ role: \"user\", content: \"Write a short story.\" }],\n  stream: true,\n});\n\n// Process the stream and convert to T.140\nprocessAIStream(stream);\n```\n\n#### Direct RTP Streaming\n\nFor direct RTP streaming without needing a WebSocket intermediary:\n\n```typescript\nimport { processAIStreamToRtp } from \"t140llm\";\nimport { OpenAI } from \"openai\";\n\n// Initialize your LLM client\nconst openai = new OpenAI({\n  apiKey: process.env.OPENAI_API_KEY,\n});\n\n// Create a streaming response\nconst stream = await openai.chat.completions.create({\n  model: \"gpt-4\",\n  messages: [{ role: \"user\", content: \"Write a short story.\" }],\n  stream: true,\n});\n\n// Stream directly to a remote endpoint using RTP\nconst transport = processAIStreamToRtp(\n  stream,\n  \"192.168.1.100\", // Remote IP address\n  5004, // RTP port (optional, default: 5004)\n  {\n    payloadType: 96, // T.140 payload type (optional)\n    ssrc: 12345, // RTP SSRC identifier (optional)\n    initialSequenceNumber: 0, // Starting sequence number (optional)\n    initialTimestamp: 0, // Starting timestamp (optional)\n    timestampIncrement: 160, // Timestamp increment per packet (optional)\n  },\n);\n\n// Later, you can close the transport if needed\n// transport.close();\n```\n\n#### Secure SRTP Streaming\n\nFor secure SRTP streaming:\n\n```typescript\nimport { processAIStreamToSrtp, createSrtpKeysFromPassphrase } from \"t140llm\";\nimport { OpenAI } from \"openai\";\n\n// Initialize your LLM client\nconst openai = new OpenAI({\n  apiKey: process.env.OPENAI_API_KEY,\n});\n\n// Create a streaming response\nconst stream = await openai.chat.completions.create({\n  model: \"gpt-4\",\n  messages: [{ role: \"user\", content: \"Write a short story.\" }],\n  stream: true,\n});\n\n// Generate SRTP keys from a passphrase\n// In a real application, you would exchange these securely with the remote endpoint\nconst { masterKey, masterSalt } = createSrtpKeysFromPassphrase(\n  \"your-secure-passphrase\",\n);\n\n// Stream directly to a remote endpoint using SRTP\nconst transport = processAIStreamToSrtp(\n  stream,\n  \"192.168.1.100\", // Remote IP address\n  5006, // SRTP port (optional, default: 5006)\n  {\n    masterKey, // SRTP master key\n    masterSalt, // SRTP master salt\n    payloadType: 96, // T.140 payload type (optional)\n  },\n);\n\n// Later, you can close the transport if needed\n// transport.close();\n```\n\n#### With Forward Error Correction\n\nFor RTP streaming with Forward Error Correction (FEC) according to RFC 5109:\n\n```typescript\nimport { processAIStreamToRtp } from \"t140llm\";\nimport { OpenAI } from \"openai\";\n\n// Initialize your LLM client\nconst openai = new OpenAI({\n  apiKey: process.env.OPENAI_API_KEY,\n});\n\n// Create a streaming response\nconst stream = await openai.chat.completions.create({\n  model: \"gpt-4\",\n  messages: [{ role: \"user\", content: \"Write a short story.\" }],\n  stream: true,\n});\n\n// Stream directly to a remote endpoint using RTP with FEC enabled\nconst transport = processAIStreamToRtp(\n  stream,\n  \"192.168.1.100\", // Remote IP address\n  5004, // RTP port (optional, default: 5004)\n  {\n    payloadType: 96, // T.140 payload type\n    ssrc: 12345, // RTP SSRC identifier\n    // FEC configuration\n    fecEnabled: true, // Enable Forward Error Correction\n    fecPayloadType: 97, // Payload type for FEC packets\n    fecGroupSize: 5, // Number of media packets to protect with one FEC packet\n  },\n);\n\n// Later, you can close the transport if needed\n// transport.close();\n```\n\n#### With Custom Transport\n\nYou can use your own transport mechanism instead of the built-in UDP socket:\n\n```typescript\nimport { processAIStreamToRtp } from \"t140llm\";\nimport { OpenAI } from \"openai\";\n\n// Initialize your LLM client\nconst openai = new OpenAI({\n  apiKey: process.env.OPENAI_API_KEY,\n});\n\n// Create a streaming response\nconst stream = await openai.chat.completions.create({\n  model: \"gpt-4\",\n  messages: [{ role: \"user\", content: \"Write a short story.\" }],\n  stream: true,\n});\n\n// Create a custom transport (e.g., WebRTC data channel, custom socket, etc.)\nclass MyCustomTransport {\n  send(data, callback) {\n    // Send the data using your custom transport mechanism\n    console.log(`Sending ${data.length} bytes`);\n    // ...your sending logic here...\n\n    // Call the callback when done (or with an error if it failed)\n    if (callback) callback();\n  }\n\n  close() {\n    // Clean up resources when done\n    console.log(\"Transport closed\");\n  }\n}\n\n// Stream using the custom transport\nconst customTransport = new MyCustomTransport();\nconst transport = processAIStreamToRtp(\n  stream,\n  \"dummy-address\", // Not used with custom transport\n  5004, // Not used with custom transport\n  {\n    customTransport, // Your custom transport implementation\n    payloadType: 96,\n    redEnabled: true, // You can still use features like redundancy with custom transport\n  },\n);\n\n// The transport will be closed automatically when the stream ends\n```\n\n#### Pre-connecting to Transport\n\nYou can establish the transport connection before the LLM stream is available, which can reduce latency when the stream starts:\n\n```typescript\nimport { createT140WebSocketTransport } from \"t140llm\";\n\n// Create the WebSocket connection early, before the LLM stream is available\nconst { connection, attachStream } = createT140WebSocketTransport(\n  \"ws://localhost:5004\",\n);\n\n// Later, when the LLM stream becomes available, attach it to the existing connection\nfunction handleLLMResponse(llmStream) {\n  // Attach the stream to the pre-created connection\n  attachStream(llmStream, {\n    processBackspaces: true,\n    handleMetadata: true,\n  });\n}\n\n// Similar pre-connection functions are available for all transport types:\n// - createDirectSocketTransport()\n// - createT140RtpTransport()\n// - createT140SrtpTransport()\n```\n\nThis is especially useful in scenarios where:\n\n1. You want to establish the connection in advance to minimize latency\n2. You need to reuse the same transport for multiple LLM streams\n3. Your architecture needs to separate transport creation from stream processing\n\nSee the [examples/pre_connect_example.js](examples/pre_connect_example.js) file for complete examples of pre-connecting with different transport types.\n\n#### Multiplexing Multiple LLM Streams\n\nYou can combine multiple LLM streams into a single RTP output stream using the multiplexer:\n\n```typescript\nimport { processAIStreamsToMultiplexedRtp, createT140RtpMultiplexer, addAIStreamToMultiplexer } from \"t140llm\";\nimport { OpenAI } from \"openai\";\nimport Anthropic from \"@anthropic-ai/sdk\";\n\n// Initialize clients\nconst openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });\nconst anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });\n\n// Create streaming responses from different models\nconst stream1 = await openai.chat.completions.create({\n  model: \"gpt-4\",\n  messages: [{ role: \"user\", content: \"Write a short story about robots.\" }],\n  stream: true,\n});\n\nconst stream2 = await anthropic.messages.create({\n  model: \"claude-3-sonnet-20240229\",\n  messages: [{ role: \"user\", content: \"Write a short poem about nature.\" }],\n  stream: true,\n});\n\n// Method 1: Use the convenience function to multiplex streams\nconst streams = new Map();\nstreams.set('gpt4', stream1);\nstreams.set('claude', stream2);\n\nconst multiplexer = processAIStreamsToMultiplexedRtp(\n  streams,\n  \"192.168.1.100\", // Remote IP address\n  5004, // RTP port\n  {\n    multiplexEnabled: true, // Required for multiplexing\n    useCsrcForStreamId: true, // Use CSRC field for stream identification (recommended)\n    charRateLimit: 60, // Higher rate limit for multiple streams\n  }\n);\n\n// Method 2: Create multiplexer first, then add streams dynamically\nconst multiplexer = createT140RtpMultiplexer(\n  \"192.168.1.100\", // Remote IP address\n  5004, // RTP port\n  {\n    multiplexEnabled: true,\n    useCsrcForStreamId: true,\n  }\n);\n\n// Add streams with unique identifiers\nmultiplexer.addStream('gpt4', stream1);\nmultiplexer.addStream('claude', stream2);\n\n// Add another stream later when it becomes available\nconst stream3 = await openai.chat.completions.create({\n  model: \"gpt-3.5-turbo\",\n  messages: [{ role: \"user\", content: \"Write a joke.\" }],\n  stream: true,\n});\n\naddAIStreamToMultiplexer(multiplexer, 'gpt35', stream3);\n\n// Listen for multiplexer events\nmultiplexer.on('streamAdded', (id) =\u003e {\n  console.log(`Stream added: ${id}`);\n});\n\nmultiplexer.on('streamRemoved', (id) =\u003e {\n  console.log(`Stream removed: ${id}`);\n});\n\nmultiplexer.on('streamError', ({ streamId, error }) =\u003e {\n  console.error(`Error in stream ${streamId}:`, error);\n});\n\n// Close multiplexer when done\n// multiplexer.close();\n```\n\nOn the receiving end, you can use the `T140StreamDemultiplexer` to extract the original streams:\n\n```typescript\nimport { T140StreamDemultiplexer } from \"t140llm\";\nimport * as dgram from \"dgram\";\n\n// Create a UDP socket to receive RTP packets\nconst socket = dgram.createSocket('udp4');\n\n// Create demultiplexer\nconst demultiplexer = new T140StreamDemultiplexer();\n\n// Process incoming RTP packets\nsocket.on('message', (msg) =\u003e {\n  // Process the packet through the demultiplexer\n  demultiplexer.processPacket(msg, true); // Use true for CSRC-based identification\n});\n\n// Listen for new streams\ndemultiplexer.on('stream', (streamId, stream) =\u003e {\n  console.log(`New stream detected: ${streamId}`);\n  \n  // Handle this stream's data\n  stream.on('data', (text) =\u003e {\n    console.log(`[${streamId}] ${text}`);\n  });\n  \n  stream.on('metadata', (metadata) =\u003e {\n    console.log(`[${streamId}] Metadata:`, metadata);\n  });\n});\n\n// Bind socket to listen for packets\nsocket.bind(5004);\n```\n\nThe multiplexing feature provides two methods for identifying streams:\n\n1. **CSRC field identification** (recommended): Uses the RTP CSRC field to carry stream identifiers\n2. **Prefix-based identification**: Prepends each payload with a stream identifier\n\nSee the [examples/multiplexed_streams_example.js](examples/multiplexed_streams_example.js) file for a complete example of multiplexing multiple LLM streams.\n\n##### Why Multiplex?\n\nThere are numerous benefits to implementing T140LLM's multiplexed streams, among a few are:\n\n**Network efficiency benefits:**\n  - Transmit multiple AI model outputs over a single connection\n  - Implement fair bandwidth allocation across streams\n  - Dynamic stream handling - add or remove AI streams at runtime\n  - Identification preservation - maintain separation between different model outputs\n  - Simplified integration - manage multiple AI services through a unified protocol\n\n**Fastest-first benefits:**\n  - Implement first-response selection - use whichever model responds first\n  - Reduce perceived latency - display the fastest response immediately\n  - Enable progressive enhancement - show initial results quickly while better/slower models complete\n\n**Security benefits:**\n  - Double encryption through nested SRTP - outer layer for transport, inner layer for individual streams\n  - Defense-in-depth protection requiring compromise of multiple encryption keys\n  - Compartmentalization of security domains for different AI providers\n  - Protection against side-channel attacks through multiple encryption layers\n\n**Content Personalization benefits:**\n  - Client-side stream selection based on user preferences/needs\n  - Adaptive content delivery (education level, technical depth, etc.)\n  - Multi-language support from a single source\n  - Real-time translation alongside original content\n\n#### With Reasoning Stream Processing\n\nSome LLM providers can stream their reasoning process as separate metadata alongside the generated text. This allows applications to show both the LLM's thought process and its final output:\n\n```typescript\nimport { processAIStream } from \"t140llm\";\nimport Anthropic from \"@anthropic-ai/sdk\";\n\n// Initialize Anthropic client\nconst anthropic = new Anthropic({\n  apiKey: process.env.ANTHROPIC_API_KEY,\n});\n\n// Create a streaming response with reasoning\nconst stream = await anthropic.messages.create({\n  model: \"claude-3-sonnet-20240229\",\n  messages: [{ role: \"user\", content: \"Solve this math problem: 2x + 5 = 13\" }],\n  stream: true,\n});\n\n// Create a custom reasoning handler\nconst handleReasoning = (metadata) =\u003e {\n  if (metadata.type === \"reasoning\") {\n    console.log(\"REASONING:\", metadata.content);\n  }\n};\n\n// Process the stream with reasoning handling\nprocessAIStream(stream, \"ws://localhost:3000\", {\n  handleMetadata: true,\n  metadataCallback: handleReasoning,\n  sendMetadataOverWebsocket: true, // Also send reasoning over WebSocket\n});\n```\n\nFor more advanced usage, including separate transports for text and reasoning, see the [examples/reasoning_example.js](examples/reasoning_example.js) and [examples/reasoning_direct_socket_example.js](examples/reasoning_direct_socket_example.js) examples.\n\n## Why?\n\nThe T.140 protocol is a well-defined standard for transmitting text conversations\nover IP networks in real-time, making it an effective way to transmit text as\nit is being written to satelites, noisy environments, and environments where\nlow latency transmission is a requirement. Unlike other approaches, the T.140\nstandard enables transmission of text before the entire message has been both\ncomposed and sent.\n\nBecause LLMs do not make mistakes while \"typing,\" there is no true downside to\nusing such an approach for transmitting the data they output. That said, we _did_\nprovide support for backspace characters, should you require this! Using T.140,\nyou can both reduce the overall file size of packets being delivered, and improve\nyour quality of experience when latency is a particularly sensitive measurement. Typically you can expect at minimum a 10% reduction in latency compared with websockets.\n\n## How It Works\n\n### WebSocket Mode\n\n1. The library sets up a WebSocket server to receive text chunks.\n2. When an LLM stream is processed, each text chunk is sent through the WebSocket.\n3. The WebSocket server encapsulates the text in T.140 format using RTP packets.\n4. The RTP packets are sent through a Unix SEQPACKET socket.\n5. Your application can read from this socket to get the real-time text data.\n\n### Direct RTP Mode\n\n1. The library creates a UDP socket to send RTP packets.\n2. When an LLM stream is processed, each text chunk is packaged as T.140 in an RTP packet.\n3. The RTP packets are sent directly to the specified IP address and port.\n4. If Forward Error Correction (FEC) is enabled, the library will:\n   - Store packets in a buffer\n   - Generate FEC packets using XOR-based operations following RFC 5109\n   - Send FEC packets at configured intervals (based on group size)\n5. Your application can receive these packets directly from the UDP socket, using FEC packets to recover from packet loss.\n\n### Secure SRTP Mode\n\n1. The library creates a UDP socket and initializes an SRTP session with the provided keys.\n2. When an LLM stream is processed, each text chunk is packaged as T.140 in an RTP packet.\n3. The RTP packets are encrypted using SRTP with the configured encryption parameters.\n4. The encrypted SRTP packets are sent to the specified IP address and port.\n5. Your application can decrypt and receive these packets using the same SRTP parameters.\n\n## API Reference\n\n### processAIStream(stream, [websocketUrl])\n\n- `stream` \u003cTextDataStream\u003e The streaming data source — an EventEmitter emitting text chunks, or an AsyncIterable (e.g., direct LLM SDK stream).\n- `websocketUrl` \u003c[string][string-mdn-url]\u003e Optional. WebSocket URL to connect to. Defaults to `ws://localhost:8765`.\n- returns: \u003cvoid\u003e\n\nProcesses an AI stream and sends the text chunks as T.140 data through a WebSocket.\n\n### processAIStreamToRtp(stream, remoteAddress, [remotePort], [rtpConfig])\n\n- `stream` \u003cTextDataStream\u003e The streaming data source — an EventEmitter emitting text chunks, or an AsyncIterable (e.g., direct LLM SDK stream).\n- `remoteAddress` \u003c[string][string-mdn-url]\u003e The remote IP address to send RTP packets to. Only used if no custom transport is provided.\n- `remotePort` \u003c[number][number-mdn-url]\u003e Optional. The remote port to send RTP packets to. Defaults to `5004`. Only used if no custom transport is provided.\n- `rtpConfig` \u003cRtpConfig\u003e Optional. Configuration options for RTP:\n  - `payloadType` \u003c[number][number-mdn-url]\u003e Optional. The RTP payload type. Defaults to `96`.\n  - `ssrc` \u003c[number][number-mdn-url]\u003e Optional. The RTP synchronization source. Defaults to a cryptographically secure random value.\n  - `initialSequenceNumber` \u003c[number][number-mdn-url]\u003e Optional. The initial sequence number. Defaults to `0`.\n  - `initialTimestamp` \u003c[number][number-mdn-url]\u003e Optional. The initial timestamp. Defaults to `0`.\n  - `timestampIncrement` \u003c[number][number-mdn-url]\u003e Optional. The timestamp increment per packet. Defaults to `160`.\n  - `fecEnabled` \u003c[boolean][boolean-mdn-url]\u003e Optional. Enable Forward Error Correction. Defaults to `false`.\n  - `fecPayloadType` \u003c[number][number-mdn-url]\u003e Optional. The payload type for FEC packets. Defaults to `97`.\n\n### createT140WebSocketTransport(websocketUrl, [options])\n\n- `websocketUrl` \u003c[string][string-mdn-url]\u003e Optional. WebSocket URL to connect to. Defaults to `ws://localhost:8765`.\n- `options` \u003cObject\u003e Optional. Configuration options:\n  - `tlsOptions` \u003cObject\u003e Optional. SSL/TLS options for secure WebSocket connections.\n- returns: \u003cObject\u003e An object containing:\n  - `connection` \u003cWebSocket\u003e The WebSocket connection\n  - `attachStream` \u003cFunction\u003e A function to attach a TextDataStream to this connection\n\nCreates a WebSocket connection that can be used for T.140 transport. This allows establishing the connection before the LLM stream is available.\n\n### createDirectSocketTransport(socketPath, [rtpConfig])\n\n- `socketPath` \u003c[string][string-mdn-url]\u003e Optional. Path to the SEQPACKET socket. Defaults to the library's default socket path.\n- `rtpConfig` \u003cRtpConfig\u003e Optional. Configuration options for RTP (same as in processAIStreamToRtp).\n- returns: \u003cObject\u003e An object containing:\n  - `transport` \u003cSocket|TransportStream\u003e The direct socket or custom transport\n  - `attachStream` \u003cFunction\u003e A function to attach a TextDataStream to this transport\n  - `rtpState` \u003cObject\u003e Current RTP state (sequence number, timestamp, ssrc)\n\nCreates a direct socket transport that can be used for T.140 RTP transmission. This allows establishing the connection before the LLM stream is available.\n\n### createT140RtpTransport(remoteAddress, [remotePort], [rtpConfig])\n\n- `remoteAddress` \u003c[string][string-mdn-url]\u003e The remote IP address to send RTP packets to.\n- `remotePort` \u003c[number][number-mdn-url]\u003e Optional. The remote port to send RTP packets to. Defaults to `5004`.\n- `rtpConfig` \u003cRtpConfig\u003e Optional. Configuration options for RTP (same as in processAIStreamToRtp).\n- returns: \u003cObject\u003e An object containing:\n  - `transport` \u003cT140RtpTransport\u003e The RTP transport instance\n  - `attachStream` \u003cFunction\u003e A function to attach a TextDataStream to this transport\n\nCreates an RTP transport that can be used for T.140 transmission. This allows establishing the connection before the LLM stream is available.\n\n### createT140SrtpTransport(remoteAddress, [remotePort], srtpConfig)\n\n- `remoteAddress` \u003c[string][string-mdn-url]\u003e The remote IP address to send SRTP packets to.\n- `remotePort` \u003c[number][number-mdn-url]\u003e Optional. The remote port to send SRTP packets to. Defaults to `5006`.\n- `srtpConfig` \u003cSrtpConfig\u003e SRTP configuration with master key and salt.\n- returns: \u003cObject\u003e An object containing:\n  - `transport` \u003cT140RtpTransport\u003e The RTP transport instance configured for SRTP\n  - `attachStream` \u003cFunction\u003e A function to attach a TextDataStream to this transport\n\nCreates an SRTP transport that can be used for secure T.140 transmission. This allows establishing the connection before the LLM stream is available.\n\n### createT140RtpMultiplexer(remoteAddress, [remotePort], [multiplexConfig])\n\n- `remoteAddress` \u003c[string][string-mdn-url]\u003e The remote IP address to send multiplexed packets to.\n- `remotePort` \u003c[number][number-mdn-url]\u003e Optional. The remote port to send multiplexed packets to. Defaults to `5004`.\n- `multiplexConfig` \u003cRtpConfig\u003e Optional. Configuration options for the multiplexer:\n  - `multiplexEnabled` \u003c[boolean][boolean-mdn-url]\u003e Required. Set to `true` to enable multiplexing.\n  - `useCsrcForStreamId` \u003c[boolean][boolean-mdn-url]\u003e Optional. Use CSRC field for stream identification. Defaults to `false`.\n  - `charRateLimit` \u003c[number][number-mdn-url]\u003e Optional. Character rate limit for all streams combined. Defaults to `30`.\n  - All other RTP configuration options are also supported.\n- returns: \u003cT140RtpMultiplexer\u003e The multiplexer instance.\n\nCreates a multiplexer that can combine multiple LLM streams into a single RTP output.\n\n### processAIStreamToRtp(stream, remoteAddress, [remotePort], [rtpConfig])\n\n- `stream` \u003cTextDataStream\u003e The streaming data source — an EventEmitter emitting text chunks, or an AsyncIterable (e.g., direct LLM SDK stream).\n- `remoteAddress` \u003c[string][string-mdn-url]\u003e The remote IP address to send RTP packets to.\n- `remotePort` \u003c[number][number-mdn-url]\u003e Optional. The remote port to send RTP packets to. Defaults to `5004`.\n- `rtpConfig` \u003cRtpConfig\u003e Optional. Configuration options for RTP:\n  - `payloadType` \u003c[number][number-mdn-url]\u003e Optional. The RTP payload type. Defaults to `96`.\n  - `ssrc` \u003c[number][number-mdn-url]\u003e Optional. The RTP synchronization source. Defaults to a cryptographically secure random value.\n  - `initialSequenceNumber` \u003c[number][number-mdn-url]\u003e Optional. The initial sequence number. Defaults to `0`.\n  - `initialTimestamp` \u003c[number][number-mdn-url]\u003e Optional. The initial timestamp. Defaults to `0`.\n  - `timestampIncrement` \u003c[number][number-mdn-url]\u003e Optional. The timestamp increment per packet. Defaults to `160`.\n  - `fecEnabled` \u003c[boolean][boolean-mdn-url]\u003e Optional. Enable Forward Error Correction. Defaults to `false`.\n  - `fecPayloadType` \u003c[number][number-mdn-url]\u003e Optional. The payload type for FEC packets. Defaults to `97`.\n  - `fecGroupSize` \u003c[number][number-mdn-url]\u003e Optional. Number of media packets to protect with one FEC packet. Defaults to `5`.\n  - `customTransport` \u003cTransportStream\u003e Optional. A custom transport implementation to use instead of the default UDP socket.\n- returns: \u003cT140RtpTransport\u003e The transport object that can be used to close the connection.\n\nProcesses an AI stream and sends the text chunks directly as T.140 data over RTP. When FEC is enabled, it adds Forward Error Correction packets according to RFC 5109 to help recover from packet loss. If a custom transport is provided, it will be used instead of creating a UDP socket.\n\n### processAIStreamToSrtp(stream, remoteAddress, [remotePort], srtpConfig)\n\n- `stream` \u003cTextDataStream\u003e The streaming data source — an EventEmitter emitting text chunks, or an AsyncIterable (e.g., direct LLM SDK stream).\n- `remoteAddress` \u003c[string][string-mdn-url]\u003e The remote IP address to send SRTP packets to. Only used if no custom transport is provided.\n- `remotePort` \u003c[number][number-mdn-url]\u003e Optional. The remote port to send SRTP packets to. Defaults to `5006`. Only used if no custom transport is provided.\n- `srtpConfig` \u003cSrtpConfig\u003e SRTP configuration including master key and salt.\n  - `masterKey` \u003cBuffer\u003e Required. The SRTP master key.\n  - `masterSalt` \u003cBuffer\u003e Required. The SRTP master salt.\n  - `profile` \u003cSrtpProtectionProfile\u003e Optional. The SRTP crypto profile (valid values: 0x0001–0x0008).\n  - `customTransport` \u003cTransportStream\u003e Optional. A custom transport implementation to use instead of the default UDP socket.\n- returns: \u003cT140RtpTransport\u003e The transport object that can be used to close the connection.\n\nProcesses an AI stream and sends the text chunks directly as T.140 data over secure SRTP. If a custom transport is provided, it will be used instead of creating a UDP socket.\n\n### processAIStreamsToMultiplexedRtp(streams, remoteAddress, [remotePort], [rtpConfig])\n\n- `streams` \u003cMap\u003cstring, TextDataStream\u003e\u003e A map of stream IDs to TextDataStream instances.\n- `remoteAddress` \u003c[string][string-mdn-url]\u003e The remote IP address to send RTP packets to.\n- `remotePort` \u003c[number][number-mdn-url]\u003e Optional. The remote port to send RTP packets to. Defaults to `5004`.\n- `rtpConfig` \u003cRtpConfig\u003e Optional. Configuration options for RTP, including multiplexing options:\n  - `multiplexEnabled` \u003c[boolean][boolean-mdn-url]\u003e Required. Set to `true` to enable multiplexing.\n  - `useCsrcForStreamId` \u003c[boolean][boolean-mdn-url]\u003e Optional. Use CSRC field for stream identification. Defaults to `false`.\n  - All other RTP configuration options are also supported.\n- returns: \u003cT140RtpMultiplexer\u003e The multiplexer instance.\n\nProcesses multiple AI streams and combines them into a single multiplexed RTP output.\n\n### createRtpPacket(sequenceNumber, timestamp, payload, [options])\n\n- `sequenceNumber` \u003c[number][number-mdn-url]\u003e RTP sequence number.\n- `timestamp` \u003c[number][number-mdn-url]\u003e RTP timestamp.\n- `payload` \u003c[string][string-mdn-url]\u003e Text payload to encapsulate.\n- `options` \u003cPartial\u003cRtpConfig\u003e\u003e Optional. Configuration options for the RTP packet.\n- returns: \u003cBuffer\u003e RTP packet with T.140 payload.\n\nCreates an RTP packet with a T.140 payload.\n\n### createSrtpKeysFromPassphrase(passphrase)\n\n- `passphrase` \u003c[string][string-mdn-url]\u003e A passphrase to derive SRTP keys from.\n- returns: \u003cObject\u003e An object containing the `masterKey` and `masterSalt` for SRTP.\n\nCreates SRTP master key and salt from a passphrase. For production, use a more secure key derivation function.\n\n### T140RtpTransport\n\nA class that manages RTP/SRTP connections for sending T.140 data.\n\n#### constructor(remoteAddress, [remotePort], [config])\n\n- `remoteAddress` \u003c[string][string-mdn-url]\u003e The remote IP address to send packets to. Only used if no custom transport is provided.\n- `remotePort` \u003c[number][number-mdn-url]\u003e Optional. The remote port to send packets to. Defaults to `5004`. Only used if no custom transport is provided.\n- `config` \u003cRtpConfig\u003e Optional. Configuration options for RTP, including FEC options and custom transport.\n  - `customTransport` \u003cTransportStream\u003e Optional. A custom transport implementation to use instead of the default UDP socket.\n\n#### setupSrtp(srtpConfig)\n\n- `srtpConfig` \u003cSrtpConfig\u003e SRTP configuration including master key and salt.\n- returns: \u003cvoid\u003e\n\nInitializes and configures SRTP for secure transmission.\n\n#### sendText(text)\n\n- `text` \u003c[string][string-mdn-url]\u003e The text to send as T.140.\n- returns: \u003cvoid\u003e\n\nSends text data as T.140 over RTP or SRTP. If FEC is enabled, it will also generate and send FEC packets according to the configured group size.\n\n#### close()\n\n- returns: \u003cvoid\u003e\n\nCloses the UDP socket or custom transport and cleans up resources. If FEC is enabled, it will send any remaining FEC packets before closing.\n\n### T140RtpMultiplexer\n\nA class that manages multiple LLM streams and multiplexes them into a single RTP output.\n\n#### constructor(remoteAddress, [remotePort], [config])\n\n- `remoteAddress` \u003c[string][string-mdn-url]\u003e The remote IP address to send packets to.\n- `remotePort` \u003c[number][number-mdn-url]\u003e Optional. The remote port to send packets to. Defaults to `5004`.\n- `config` \u003cRtpConfig\u003e Optional. Configuration options for the multiplexer.\n\n#### addStream(id, stream, [streamConfig], [processorOptions])\n\n- `id` \u003c[string][string-mdn-url]\u003e Unique identifier for this stream.\n- `stream` \u003cTextDataStream\u003e The stream to add to the multiplexer.\n- `streamConfig` \u003cRtpConfig\u003e Optional. Configuration options specific to this stream.\n- `processorOptions` \u003cProcessorOptions\u003e Optional. Options for processing this stream.\n- returns: \u003c[boolean][boolean-mdn-url]\u003e `true` if the stream was added successfully, `false` otherwise.\n\nAdds a new stream to the multiplexer.\n\n#### removeStream(id)\n\n- `id` \u003c[string][string-mdn-url]\u003e ID of the stream to remove.\n- returns: \u003c[boolean][boolean-mdn-url]\u003e `true` if the stream was found and removed, `false` otherwise.\n\nRemoves a stream from the multiplexer.\n\n#### getStreamCount()\n\n- returns: \u003c[number][number-mdn-url]\u003e The number of active streams.\n\nReturns the number of active streams in the multiplexer.\n\n#### getStreamIds()\n\n- returns: \u003cArray\u003c[string][string-mdn-url]\u003e\u003e Array of active stream IDs.\n\nReturns an array of all active stream IDs.\n\n#### close()\n\n- returns: \u003cvoid\u003e\n\nCloses the multiplexer and all streams.\n\n#### Events\n\n- `streamAdded` - Emitted when a new stream is added to the multiplexer.\n- `streamRemoved` - Emitted when a stream is removed from the multiplexer.\n- `streamError` - Emitted when an error occurs with a specific stream.\n- `metadata` - Emitted when metadata is received from any stream.\n- `error` - Emitted when an error occurs with the multiplexer itself.\n\n### T140StreamDemultiplexer\n\nA class that extracts individual streams from multiplexed RTP packets.\n\n#### constructor()\n\nCreates a new demultiplexer instance.\n\n#### processPacket(data, useCSRC)\n\n- `data` \u003cBuffer\u003e Buffer containing RTP packet data.\n- `useCSRC` \u003c[boolean][boolean-mdn-url]\u003e Optional. Whether to use CSRC fields for stream identification. Defaults to `false`.\n- returns: \u003cvoid\u003e\n\nProcesses an RTP packet and extracts stream information.\n\n#### getStream(streamId)\n\n- `streamId` \u003c[string][string-mdn-url]\u003e The stream ID to retrieve.\n- returns: \u003cDemultiplexedStream|undefined\u003e The demultiplexed stream if found, `undefined` otherwise.\n\nGets a demultiplexed stream by ID.\n\n#### getStreamIds()\n\n- returns: \u003cArray\u003c[string][string-mdn-url]\u003e\u003e Array of detected stream IDs.\n\nReturns an array of all detected stream IDs.\n\n#### Events\n\n- `stream` - Emitted when a new stream is detected.\n- `data` - Emitted for all demultiplexed data with streamId, text, and metadata.\n- `error` - Emitted when an error occurs during packet processing.\n\n### TransportStream Interface\n\nAn interface that custom transport implementations must follow to be compatible with T140RtpTransport.\n\n#### send(data, callback)\n\n- `data` \u003cBuffer\u003e The packet data to send.\n- `callback` \u003cFunction\u003e Optional. Called when the packet has been sent or if an error occurred.\n  - `error` \u003cError\u003e Optional. The error that occurred during sending, if any.\n- returns: \u003cvoid\u003e\n\nSends a packet through the transport.\n\n#### close()\n\n- returns: \u003cvoid\u003e\n\nOptional method to close the transport and clean up resources.\n\n## License\n\n[MIT License](https://mit-license.org/) © agrathwohl\n\n\u003c!-- References --\u003e\n\n[typescript-url]: https://github.com/Microsoft/TypeScript\n[nodejs-url]: https://nodejs.org\n[npm-url]: https://www.npmjs.com\n[node-version-url]: https://nodejs.org/en/download\n[mit-license-url]: https://github.com/agrathwohl/t140llm/blob/master/LICENSE\n\n\u003c!-- MDN --\u003e\n\n[string-mdn-url]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String\n[number-mdn-url]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number\n[promise-mdn-url]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise\n[boolean-mdn-url]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean\n\n\u003c!-- Badges --\u003e\n\n[node-version-badge]: https://flat.badgen.net/npm/node/t140llm\n[mit-license-badge]: https://flat.badgen.net/npm/license/t140llm\n[tm]: https://github.com/agrathwohl/t140llm/blob/master/examples/baudot_ita2_tty_example.js\n[ad]: https://github.com/agrathwohl/assistive-llm\n[sat]: https://github.com/agrathwohl/t140llm/blob/master/examples/fec_demo.js\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fagrathwohl%2Ft140llm","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fagrathwohl%2Ft140llm","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fagrathwohl%2Ft140llm/lists"}