@rimelabs/sdk 0.1.0-alpha.22 → 0.1.0-alpha.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/README.md +6 -0
- package/dist/realtime/protocol.d.ts +2 -7
- package/dist/realtime/protocol.js +19 -0
- package/dist/realtime/session.js +1 -1
- package/dist/realtime/transport.d.ts +2 -0
- package/dist/realtime/transport.js +76 -11
- package/dist/realtime/types.d.ts +21 -1
- package/docs/realtime.md +49 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.1.0-alpha.24](https://github.com/rimelabs/rime-sdk/compare/typescript-v0.1.0-alpha.23...typescript-v0.1.0-alpha.24) (2026-10-27)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### ⚠ BREAKING CHANGES
|
|
7
|
+
|
|
8
|
+
* Python and TypeScript SessionEvent payload unions now include ItemTruncated. Update exhaustive event handlers to accept ItemTruncated or the item.truncated kind. Handle unavailable retained text separately from an empty string. Rust's event enum remains non-exhaustive.
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
|
|
12
|
+
* expose realtime truncation events and summaries ([#112](https://github.com/rimelabs/rime-sdk/issues/112)) ([15d6802](https://github.com/rimelabs/rime-sdk/commit/15d6802f71c849ce08b1d0b46e78bd44d5dfc3b7))
|
|
13
|
+
|
|
14
|
+
## [0.1.0-alpha.23](https://github.com/rimelabs/rime-sdk/compare/typescript-v0.1.0-alpha.22...typescript-v0.1.0-alpha.23) (2026-10-27)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Bug Fixes
|
|
18
|
+
|
|
19
|
+
* preserve Prism handshake error categories and diagnostics ([#110](https://github.com/rimelabs/rime-sdk/issues/110)) ([75568b2](https://github.com/rimelabs/rime-sdk/commit/75568b278afddd265a0a4f2c26c716ccb6b29514))
|
|
20
|
+
|
|
3
21
|
## [0.1.0-alpha.22](https://github.com/rimelabs/rime-sdk/compare/typescript-v0.1.0-alpha.21...typescript-v0.1.0-alpha.22) (2026-10-24)
|
|
4
22
|
|
|
5
23
|
|
package/README.md
CHANGED
|
@@ -28,6 +28,12 @@ STT use the SDK and Node.js built-ins.
|
|
|
28
28
|
|
|
29
29
|
## Prism: send recorded speech
|
|
30
30
|
|
|
31
|
+
If Prism refuses the connection with HTTP 429, `realtime.connect()` throws
|
|
32
|
+
`RimeResourceLimitError`. HTTP 503 throws `RimeUnavailableError`. Handshake
|
|
33
|
+
errors include the HTTP status, the server request ID when supplied, and a
|
|
34
|
+
short server explanation when available. Missing or invalid error bodies use
|
|
35
|
+
a default message. The SDK does not retry the connection automatically.
|
|
36
|
+
|
|
31
37
|
With `RIME_API_KEY` set, the SDK connects to
|
|
32
38
|
`wss://prism.api.rime.ai/v1/realtime` and uses the server's default voice.
|
|
33
39
|
Pass `endpoint` or `voice` to select another deployment or voice.
|
|
@@ -76,15 +76,10 @@ export interface ItemDeleted {
|
|
|
76
76
|
export interface AudioCleared {
|
|
77
77
|
kind: "audio.cleared";
|
|
78
78
|
}
|
|
79
|
-
export interface ItemTruncated {
|
|
80
|
-
kind: "item.truncated";
|
|
81
|
-
itemId: string;
|
|
82
|
-
contentIndex: number;
|
|
83
|
-
}
|
|
84
79
|
export interface ResponseEnded extends t.ResponseEnded {
|
|
85
80
|
drainToken: string | null;
|
|
86
81
|
}
|
|
87
|
-
export type Acknowledgment = SessionUpdated | HistoryListed | ItemDeleted | ItemCreated | ToolResultCreated | AudioCleared | ItemTruncated | t.ResponseStarted | ResponseEnded;
|
|
82
|
+
export type Acknowledgment = SessionUpdated | HistoryListed | ItemDeleted | ItemCreated | ToolResultCreated | AudioCleared | t.ItemTruncated | t.ResponseStarted | ResponseEnded;
|
|
88
83
|
export type ServerPayload = SessionCreated | Acknowledgment | t.InputEvent<t.NormalizedRealtimeFault> | t.MessageStarted | t.TextDelta | t.TextDone | t.AudioDelta | t.AudioDone | t.ToolCall | t.FaultEvent<t.NormalizedRealtimeFault>;
|
|
89
84
|
export interface ServerEvent {
|
|
90
85
|
eventId: string;
|
|
@@ -113,7 +108,7 @@ export declare const requests: {
|
|
|
113
108
|
text: Request<t.ResponseStarted>;
|
|
114
109
|
cancel: Request<ResponseEnded>;
|
|
115
110
|
clear: Request<AudioCleared>;
|
|
116
|
-
truncate: Request<ItemTruncated>;
|
|
111
|
+
truncate: Request<t.ItemTruncated>;
|
|
117
112
|
};
|
|
118
113
|
export declare function truncationSettings(value: t.Truncation): NonNullable<SessionSettings["truncation"]>;
|
|
119
114
|
export declare function decode(raw: string, sessionId: string | null): ServerEvent | null;
|
|
@@ -77,6 +77,20 @@ function revision(value) {
|
|
|
77
77
|
? null
|
|
78
78
|
: integer(value.prism_settings_revision, "prism_settings_revision");
|
|
79
79
|
}
|
|
80
|
+
function responseTruncation(response) {
|
|
81
|
+
if (response.prism_truncation === undefined)
|
|
82
|
+
return null;
|
|
83
|
+
const value = object(response.prism_truncation, "response.prism_truncation");
|
|
84
|
+
const count = integer(value.items_left_out, "prism_truncation.items_left_out");
|
|
85
|
+
if (count === 0)
|
|
86
|
+
return invalid("prism_truncation.items_left_out");
|
|
87
|
+
return Object.freeze({
|
|
88
|
+
itemsLeftOut: count,
|
|
89
|
+
firstKeptItemId: value.first_kept_item_id === undefined
|
|
90
|
+
? null
|
|
91
|
+
: string(value.first_kept_item_id, "prism_truncation.first_kept_item_id"),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
80
94
|
function duration(value, field) {
|
|
81
95
|
if (value == null)
|
|
82
96
|
return null;
|
|
@@ -281,6 +295,10 @@ function decodeValue(value, sessionId) {
|
|
|
281
295
|
kind: "item.truncated",
|
|
282
296
|
itemId: id(event.item_id, "item_id"),
|
|
283
297
|
contentIndex: integer(event.content_index, "content_index"),
|
|
298
|
+
audioEndMs: integer(event.audio_end_ms, "audio_end_ms"),
|
|
299
|
+
keptText: event.prism_kept_text === undefined
|
|
300
|
+
? null
|
|
301
|
+
: string(event.prism_kept_text, "prism_kept_text"),
|
|
284
302
|
};
|
|
285
303
|
break;
|
|
286
304
|
case "prism.typed_input.ready":
|
|
@@ -396,6 +414,7 @@ function decodeValue(value, sessionId) {
|
|
|
396
414
|
payload = {
|
|
397
415
|
kind: "response.ended",
|
|
398
416
|
settingsRevision: revision(response),
|
|
417
|
+
truncation: responseTruncation(response),
|
|
399
418
|
response: ref(response.id),
|
|
400
419
|
status: status === "completed" ||
|
|
401
420
|
status === "cancelled" ||
|
package/dist/realtime/session.js
CHANGED
|
@@ -1,6 +1,42 @@
|
|
|
1
1
|
import WebSocket from "ws";
|
|
2
|
-
import { RimeAuthenticationError, RimeStreamError, RimeUnavailableError, } from "../errors.js";
|
|
2
|
+
import { RimeAuthenticationError, RimeResourceLimitError, RimeStreamError, RimeUnavailableError, } from "../errors.js";
|
|
3
3
|
import { Deferred } from "./async.js";
|
|
4
|
+
function handshakeError(status, body, requestId) {
|
|
5
|
+
let errorType = RimeUnavailableError;
|
|
6
|
+
let reason = "Realtime endpoint rejected the connection";
|
|
7
|
+
if ([401, 403].includes(status)) {
|
|
8
|
+
errorType = RimeAuthenticationError;
|
|
9
|
+
reason = "Realtime endpoint rejected credentials";
|
|
10
|
+
}
|
|
11
|
+
else if (status === 429) {
|
|
12
|
+
errorType = RimeResourceLimitError;
|
|
13
|
+
reason = "Realtime connection or rate limit reached";
|
|
14
|
+
}
|
|
15
|
+
else if (status === 503) {
|
|
16
|
+
reason = "Realtime endpoint is unavailable";
|
|
17
|
+
}
|
|
18
|
+
let message = `${reason} (HTTP ${status})`;
|
|
19
|
+
if (body.length > 0 && body.length < 1024) {
|
|
20
|
+
try {
|
|
21
|
+
let detail = new TextDecoder("utf-8", { fatal: true })
|
|
22
|
+
.decode(body)
|
|
23
|
+
.trim();
|
|
24
|
+
if (typeof detail === "string" && /^[{[]/.test(detail))
|
|
25
|
+
detail = JSON.parse(detail)?.error?.message;
|
|
26
|
+
if (typeof detail === "string" &&
|
|
27
|
+
!detail.trimStart().startsWith("<") &&
|
|
28
|
+
!/[\x00-\x08\x0b\x0c\x0e-\x1f]/.test(detail)) {
|
|
29
|
+
detail = detail.trim().replace(/\s+/gu, " ");
|
|
30
|
+
if (detail)
|
|
31
|
+
message += `: ${detail}`;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// Invalid response bodies do not change the HTTP error category.
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return new errorType(message, requestId);
|
|
39
|
+
}
|
|
4
40
|
/** Private transport. The public declarations do not require ws types. */
|
|
5
41
|
export class RealtimeConnection {
|
|
6
42
|
socket;
|
|
@@ -8,6 +44,8 @@ export class RealtimeConnection {
|
|
|
8
44
|
ended = new Deferred();
|
|
9
45
|
closing = null;
|
|
10
46
|
connected = false;
|
|
47
|
+
failed = false;
|
|
48
|
+
refusal;
|
|
11
49
|
ready = this.opened.promise;
|
|
12
50
|
onMessage = () => { };
|
|
13
51
|
onFailure = () => { };
|
|
@@ -26,17 +64,39 @@ export class RealtimeConnection {
|
|
|
26
64
|
this.opened.resolve();
|
|
27
65
|
});
|
|
28
66
|
this.socket.on("unexpected-response", (request, response) => {
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
67
|
+
const chunks = [];
|
|
68
|
+
let size = 0;
|
|
69
|
+
let finished = false;
|
|
70
|
+
const finish = (complete = false) => {
|
|
71
|
+
if (finished)
|
|
72
|
+
return;
|
|
73
|
+
finished = true;
|
|
74
|
+
clearTimeout(timer);
|
|
75
|
+
this.refusal = undefined;
|
|
76
|
+
const id = response.headers["x-request-id"];
|
|
77
|
+
this.failure(handshakeError(response.statusCode ?? 0, complete ? Buffer.concat(chunks) : Buffer.alloc(0), typeof id === "string" ? id : null));
|
|
78
|
+
response.destroy();
|
|
79
|
+
request.destroy();
|
|
80
|
+
this.socket.terminate();
|
|
81
|
+
};
|
|
82
|
+
const timer = setTimeout(finish, 1000);
|
|
83
|
+
this.refusal = finish;
|
|
84
|
+
response.on("data", (chunk) => {
|
|
85
|
+
size += chunk.length;
|
|
86
|
+
if (size >= 1024)
|
|
87
|
+
finish();
|
|
88
|
+
else
|
|
89
|
+
chunks.push(chunk);
|
|
90
|
+
});
|
|
91
|
+
response.on("end", () => finish(true));
|
|
92
|
+
response.on("error", () => finish());
|
|
93
|
+
response.on("aborted", () => finish());
|
|
36
94
|
});
|
|
37
|
-
this.socket.on("error", () => this.
|
|
38
|
-
?
|
|
39
|
-
:
|
|
95
|
+
this.socket.on("error", () => this.refusal
|
|
96
|
+
? this.refusal()
|
|
97
|
+
: this.failure(!this.connected
|
|
98
|
+
? new RimeUnavailableError("Realtime handshake failed")
|
|
99
|
+
: new RimeStreamError("Invalid event or lost realtime connection")));
|
|
40
100
|
this.socket.on("message", (data, binary) => {
|
|
41
101
|
if (binary) {
|
|
42
102
|
this.failure(new RimeStreamError("Expected a JSON text event"));
|
|
@@ -45,11 +105,15 @@ export class RealtimeConnection {
|
|
|
45
105
|
this.onMessage(data.toString());
|
|
46
106
|
});
|
|
47
107
|
this.socket.on("close", () => {
|
|
108
|
+
this.refusal?.();
|
|
48
109
|
this.ended.resolve();
|
|
49
110
|
this.failure(new RimeStreamError("Realtime connection closed; reconnect is not automatic"));
|
|
50
111
|
});
|
|
51
112
|
}
|
|
52
113
|
failure(error) {
|
|
114
|
+
if (this.failed)
|
|
115
|
+
return;
|
|
116
|
+
this.failed = true;
|
|
53
117
|
this.opened.reject(error);
|
|
54
118
|
this.onFailure(error);
|
|
55
119
|
}
|
|
@@ -66,6 +130,7 @@ export class RealtimeConnection {
|
|
|
66
130
|
close() {
|
|
67
131
|
if (!this.closing) {
|
|
68
132
|
this.closing = (async () => {
|
|
133
|
+
this.refusal?.();
|
|
69
134
|
if (this.socket.readyState === WebSocket.CLOSED)
|
|
70
135
|
return;
|
|
71
136
|
const timer = setTimeout(() => this.socket.terminate(), 2000);
|
package/dist/realtime/types.d.ts
CHANGED
|
@@ -288,10 +288,20 @@ export interface ResponseAbandoned {
|
|
|
288
288
|
readonly kind: "response.abandoned";
|
|
289
289
|
readonly response: ResponseRef;
|
|
290
290
|
}
|
|
291
|
+
/** History items excluded from this reply's prompt, not deleted from history. */
|
|
292
|
+
export interface ResponseTruncation {
|
|
293
|
+
/** Number of excluded items, not turns. Already hidden items are not counted. */
|
|
294
|
+
readonly itemsLeftOut: number;
|
|
295
|
+
/** Oldest retained item; null when no earlier item was retained. */
|
|
296
|
+
readonly firstKeptItemId: string | null;
|
|
297
|
+
}
|
|
291
298
|
export interface ResponseEnded {
|
|
292
299
|
/** Settings revision published by the server; null when unavailable.
|
|
293
300
|
* Optional on caller-created values for compatibility. */
|
|
294
301
|
readonly settingsRevision?: number | null;
|
|
302
|
+
/** Null when the server reports no exclusions or the information is unavailable.
|
|
303
|
+
* Optional on caller-created values for compatibility. */
|
|
304
|
+
readonly truncation?: ResponseTruncation | null;
|
|
295
305
|
readonly kind: "response.ended";
|
|
296
306
|
readonly response: ResponseRef;
|
|
297
307
|
readonly status: "completed" | "cancelled" | "failed" | "unknown";
|
|
@@ -303,11 +313,21 @@ export interface FaultEvent<Fault extends RealtimeFault = RealtimeFault> {
|
|
|
303
313
|
}
|
|
304
314
|
export type InputEvent<Fault extends RealtimeFault = RealtimeFault> = InputReady | SpeechHint | SpeechStarted | SpeechStopped | InputCommitted | TranscriptDelta | TranscriptFinal | TranscriptFailed<Fault>;
|
|
305
315
|
export type ResponseEvent = ResponseStarted | ResponseAbandoned | MessageStarted | TextDelta | TextDone | AudioDelta | AudioDone | ToolCall | ResponseEnded;
|
|
316
|
+
/** A playback report was recorded. Retained history text can differ from audible
|
|
317
|
+
* text: replies with tool calls retain all words. */
|
|
318
|
+
export interface ItemTruncated {
|
|
319
|
+
readonly kind: "item.truncated";
|
|
320
|
+
readonly itemId: string;
|
|
321
|
+
readonly contentIndex: number;
|
|
322
|
+
readonly audioEndMs: number;
|
|
323
|
+
/** Null when unavailable, including during generation; empty if no words remain. */
|
|
324
|
+
readonly keptText: string | null;
|
|
325
|
+
}
|
|
306
326
|
export interface SessionEvent<Fault extends RealtimeFault = RealtimeFault> {
|
|
307
327
|
readonly sessionId: string;
|
|
308
328
|
readonly eventId: string;
|
|
309
329
|
readonly requestId: string | null;
|
|
310
|
-
readonly payload: InputEvent<Fault> | ResponseEvent | FaultEvent<Fault>;
|
|
330
|
+
readonly payload: InputEvent<Fault> | ResponseEvent | ItemTruncated | FaultEvent<Fault>;
|
|
311
331
|
}
|
|
312
332
|
export interface PlaybackInterrupted {
|
|
313
333
|
readonly kind: "interrupted";
|
package/docs/realtime.md
CHANGED
|
@@ -587,3 +587,52 @@ publish it.
|
|
|
587
587
|
```text
|
|
588
588
|
await session.update({ truncation: { retentionRatio: 0.9 } });
|
|
589
589
|
```
|
|
590
|
+
|
|
591
|
+
## Retained text after interruption
|
|
592
|
+
|
|
593
|
+
Available in the next SDK release. A `conversation.item.truncated` acknowledgment
|
|
594
|
+
also appears in `session.events` as `ItemTruncated`, with kind `item.truncated`.
|
|
595
|
+
The conversation runner forwards it to `onEvent`. Playback reporting does not
|
|
596
|
+
wait for event consumption.
|
|
597
|
+
|
|
598
|
+
Use `itemId`, `contentIndex`, and `audioEndMs` to identify the reported audio.
|
|
599
|
+
`keptText` is the text retained in server history. An empty string means no words
|
|
600
|
+
remain. `null` means unavailable, including older servers and replies still being
|
|
601
|
+
generated. The event does not promise a later text update. Replies with tool calls
|
|
602
|
+
retain all words, so this field can differ from the words heard during playback.
|
|
603
|
+
|
|
604
|
+
```typescript
|
|
605
|
+
function onEvent(event: SessionEvent) {
|
|
606
|
+
if (event.payload.kind === "item.truncated") {
|
|
607
|
+
const cut = event.payload;
|
|
608
|
+
if (cut.keptText !== null) {
|
|
609
|
+
console.log("Retained text", cut.itemId, JSON.stringify(cut.keptText));
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
This event can arrive after `response.ended`. It has no response reference and does
|
|
616
|
+
not require the SDK to retain that response. `text.done` still reports generated
|
|
617
|
+
text. Add `item.truncated` to exhaustive switches on `SessionEvent.payload.kind`.
|
|
618
|
+
|
|
619
|
+
## History excluded from a reply
|
|
620
|
+
|
|
621
|
+
Available in the next SDK release. `ResponseEnded.truncation` exposes the server's
|
|
622
|
+
`response.prism_truncation` summary. It contains `itemsLeftOut` and nullable
|
|
623
|
+
`firstKeptItemId`. The count refers to conversation items excluded from this
|
|
624
|
+
reply's prompt, not turns or items deleted from history. Already hidden items do
|
|
625
|
+
not count. The first ID is absent when no item from before this turn was retained.
|
|
626
|
+
|
|
627
|
+
Decoded events have `truncation: null` when the server reports no exclusions or the
|
|
628
|
+
information is unavailable, including older servers or a reply that ended before
|
|
629
|
+
usage was counted. Caller-created events may omit the property. This does not change
|
|
630
|
+
the session's truncation policy. Session events and conversation callbacks expose
|
|
631
|
+
the same immutable summary.
|
|
632
|
+
|
|
633
|
+
```typescript
|
|
634
|
+
if (event.payload.kind === "response.ended" && event.payload.truncation) {
|
|
635
|
+
const summary = event.payload.truncation;
|
|
636
|
+
console.log(summary.itemsLeftOut, summary.firstKeptItemId);
|
|
637
|
+
}
|
|
638
|
+
```
|